zoukankan      html  css  js  c++  java
  • 【转】windows操作系统同步 (Critical Section,Mutex,Semaphore,Event Object,Interlocked Variable)

    一:Critiacal_Section

    1:使用临界区的目的是确保资源每次只能被一个线程所使用。一个线程进入某个临界区,另一个线程就不能够再进入同一个临界区。临界区不是核心对象,它只存在进程的内存空间。没有所谓的句柄,只能在同一进程中的线程间完成同步。

    2:使用函数
        VOID InitializeCriticalSection(LPCRITICAL_SECTION lpCriticalSection);
        VOID DeleteCriticalSection(LPCRITICAL_SECTION lpCriticalSection);
        VOID EnterCriticalSection(LPCRITICAL_SECTION lpCriticalSection);
        VOID LeaveCriticalSection(LPCRITICAL_SECTION lpCriticalSection);
    操作顺序:
        //开始
        CRITICAL_SECTION cs;
        InitializeCriticalSection(&cs);
        //线程1:
        EnterCriticalSection(&cs);
        LeaveCriticalSection(&cs);
        //线程2:
        EnterCriticalSection(&cs);
        LeaveCriticalSection(&cs);
        //最后:
        DeleteCriticalSection(&cs);

    3:封装成类:两个线程对同一数据一读一写,因此需要让它们在这里互斥,不能同时访问。
        class InstanceLock;
        class InstanceLockBase
        {
            friend class InstanceLock;//可以正确的访问私有函数
            CRITICAL_SECTION cs;
            void Lock()//私有
            {
                ::EnterCriticalSection(&cs);//加锁
            }
            void Unlock()//私有
            {
                ::LeaveCriticalSection(&cs);//解锁
            }
        protected://继承
            InstanceLockBase()
            {
                ::InitializeCriticalSection(&cs);//构建时初始化
            }
            ~InstanceLockBase()
            {
                ::DeleteCriticalSection(&cs);//析构时释放
            }
        };

        //为了保证Lock和Unlock能成对调用,
        //C++对于构造函数和析构函数的调用是自动成对的,把对Lock和Unlock的调用专门写在一个类的构造函数和析构函数中
        class InstanceLock
        {
            InstanceLockBase* _pObj;
        public:
            InstanceLock(InstanceLockBase* pObj)
            {
                _pObj = pObj;
                if(NULL != _pObj)
                    _pObj->Lock();
            }
            ~InstanceLock()
            {
                if(NULL != _pObj)
                    _pObj->Unlock();
            }
        };

        void Say(char* text)
        {
            static int count = 0;
            SYSTEMTIME st;
            ::GetLocalTime(&st);
            printf("%03d [%02d:%02d:%02d.%03d]%s /n", ++count, st.wHour, st.wMinute, st.wSecond, st.wMilliseconds, text);
        }

        //1 让被锁类从InstanceLockBase继承
        //2 所有要访问被锁对象的代码前面声明InstanceLock的实例,并传入被锁对象的指针。
        class MyClass: public InstanceLockBase{};
        MyClass mc;

        //将InstanceLockBase中protected改称public后,也可以直接用基类
        //InstanceLockBase mc;

        DWORD CALLBACK ThreadProc(LPVOID param)
        {
            InstanceLock il(&mc);
            Say("in sub thread, lock");
            Sleep(2000);
            Say("in sub thread, unlock");
            return 0;
        }

        int _tmain(int argc, _TCHAR* argv[])
        {
            CreateThread(0, 0, ThreadProc, 0, 0, 0);
            {
                InstanceLock il(&mc);
                Say("in main thread, lock");
                Sleep(3000);
                Say("in main thread, lock");
            }

            Sleep(5000);

            return 0;
        }

    4:另一种封装方法
        class InstanceLockBase
        {
            CRITICAL_SECTION cs;
        public:
            InstanceLockBase()
            {
                InitializeCriticalSection(&cs);
            }

            ~InstanceLockBase()
            {
                DeleteCriticalSection(&cs);
            }

            void Lock()
            {
                EnterCriticalSection(&cs);
            }

            void Unlock()
            {
                LeaveCriticalSection(&cs);       
            }
        };


        template <typename LockType>
        class AutoLock
        {
            LockType& lock;
        public:
            AutoLock(LockType& _lock) : lock(_lock)
            {
                lock.Lock();
            }

            ~AutoLock()
            {
                lock.Unlock();
            }
        };
        #endif
        //使用方法
        InstanceLockBase lock;
        AutoLock<InstanceLockBase> tmplock(lock);

        //将InstanceLockBase中protected改称public后,也可以直接用基类
        InstanceLockBase lock;
        InstanceLock il(&lock);

    二:Mutex

    1:和临界区(Critical Section)的区别
    1)锁住一个未被拥有的mutex,比锁住一个未被拥有的critical section,所需花费几乎100倍的时间。因为critical section不需要进入操作系统核心。
    2)互斥器可以跨进程使用(此时应指定名称。未命名的互斥器只能在同一进程内使用)。临界区只能够在同一进程内使用。
    3)等待一个互斥器,可以指定“结束等待”的时间。临界区不可以。

    2:使用函数
        HANDLE CreateMutex();
        HANDLE OpenMutex();
        DWORD WaitForSingleObject();
        DWORD WaitForMultipleObjects();
        DWORD MsgWaitForMultipleObjects();
        BOOL ReleaseMutex();
        BOOL CloseHandle();

        HANDLE CreateMutex(LPSECURITY_ATTRIBUTES lpMutexAttributes, // 指向安全属性的指针
                           BOOL bInitialOwner, // 初始化互斥对象的所有者
                             LPCTSTR lpName); // 指向互斥对象名的指针
    注意:一旦不再需要,注意必须用CloseHandle函数将互斥体句柄关闭。从属于它的所有句柄都被关闭后,就会删除对象。
         根据lpName,系统中的任何线程都可以使用这个名称来处理该Mutex,Mutex名称对整个系统而言是全局的。

        HANDLE WINAPI OpenMutex(DWORD dwDesiredAccess,    //打开一个已经存在的Mutex。
                                BOOL bInheritHandle,
                                LPCTSTR lpName);


    3:下列说明有两个线程需要操作资源,但是一个时刻只能有一个线程操作该资源
        #define THREADCOUNT 2

        HANDLE ghMutex;

        DWORD WINAPI WriteToDatabase( LPVOID );

        void main()
        {
            HANDLE aThread[THREADCOUNT];
            DWORD ThreadID;
            int i;

            ghMutex = CreateMutex( NULL, FALSE, NULL);
            if (ghMutex == NULL)
            {
                printf("CreateMutex error: %d/n", GetLastError());
                return;
            }

            for( i=0; i < THREADCOUNT; i++ )
            {
                aThread[i] = CreateThread(NULL, 0, (LPTHREAD_START_ROUTINE) WriteToDatabase, NULL, 0, &ThreadID);
                if( aThread[i] == NULL )
                {
                    printf("CreateThread error: %d/n", GetLastError());
                    return;
                }
            }

            WaitForMultipleObjects(THREADCOUNT, aThread, TRUE, INFINITE);

            for( i=0; i < THREADCOUNT; i++ )
                CloseHandle(aThread[i]);

            CloseHandle(ghMutex);
        }

        DWORD WINAPI WriteToDatabase( LPVOID lpParam )
        {
            DWORD dwCount=0, dwWaitResult;

            while( dwCount < 20 )
            {
                //线程间只有一个能使用该信号量
                dwWaitResult = WaitForSingleObject(ghMutex,INFINITE);

                switch (dwWaitResult)
                {
                case WAIT_OBJECT_0:
                    __try
                    {
                        printf("Thread %d writing to database.../n", GetCurrentThreadId());
                        dwCount++;
                    }

                    __finally
                    {
                        if (! ReleaseMutex(ghMutex))
                        {
                        }
                    }
                    break;
                case WAIT_ABANDONED:
                    return FALSE;
                }
            }
            return TRUE;
        }

    3:多个线程访问同一数据,一部分是读,一部分是写。我们知道只有读-写或写-写同时进行时可能会出现问题,而读-读则可以同时进行,因为它们不会对数据 进行 修改,所以也有必要在C++中封装一种方便的允许读-读并发、读-写与写-写互斥的锁。要实现这种锁,使用临界区就很困难了,不如改用内核对象,这里我使 用的是互斥量(Mutex)
        class RWLock;
        class _RWLockBase
        {
            friend class RWLock;
        protected:
            virtual DWORD ReadLock(int timeout) = 0;
            virtual void ReadUnlock(int handleIndex) = 0;
            virtual DWORD WriteLock(int timeout) = 0;
            virtual void WriteUnlock() = 0;
        };

        template <int maxReadCount = 3>        //这里给一个缺省参数,尽量减少客户端代码量
        class RWLockBase: public _RWLockBase
        {
            //二是为了允许读-读并发,这里只声明一个Mutex是不够的,必须要声明多个Mutex,而且有多少个Mutex就同时允许多少个读线程并发
            HANDLE handles[maxReadCount];
            DWORD ReadLock(int timeout)   //加读锁,只要等到一个互斥量返回即可
            {
                return ::WaitForMultipleObjects(maxReadCount, handles, FALSE, timeout);
            }
            void ReadUnlock(int handleIndex) //解读锁,释放已获得的互斥量
            {
                ::ReleaseMutex(handles[handleIndex]);
            }
            DWORD WriteLock(int timeout)   //加写锁,等到所有互斥量,从而与其他所有线程互斥
            {
                return ::WaitForMultipleObjects(maxReadCount, handles, TRUE, timeout);
            }
            void WriteUnlock()                      //解写锁,释放所有的互斥量
            {
                for(int i = 0; i < maxReadCount; i++)
                    ::ReleaseMutex(handles[i]);
            }
        protected:
            RWLockBase()                            //构造函数,初始化每个互斥量
            {
                for(int i = 0; i < maxReadCount; i++)
                    handles[i] = ::CreateMutex(0, FALSE, 0);
            }
            ~RWLockBase()                          //析构函数,销毁对象
            {
                for(int i = 0; i < maxReadCount; i++)
                    ::CloseHandle(handles[i]);
            }
        };

        class RWLock
        {
            bool lockSuccess;           //因为有可能超时,需要保存是否等待成功
            int readLockHandleIndex;    //对于读锁,需要知道获得的是哪个互斥量
            _RWLockBase* _pObj;         //目标对象基类指针
        public:
            //这里通过第二个参数决定是加读锁还是写锁,第三个参数为超时的时间
            RWLock(_RWLockBase* pObj, bool readLock = true, int timeout = 3000)
            {
                _pObj = pObj;
                lockSuccess = FALSE;
                readLockHandleIndex = -1;
                if(NULL == _pObj)
                    return;

                if(readLock)          //读锁
                {
                    DWORD retval = _pObj->ReadLock(timeout);
                    if(retval < WAIT_ABANDONED) //返回值小于WAIT_ABANDONED表示成功
                    {                                               //其值减WAIT_OBJECT_0就是数组下标
                        readLockHandleIndex = retval - WAIT_OBJECT_0;
                        lockSuccess = TRUE;
                    }
                }
                else
                {
                    DWORD retval = _pObj->WriteLock(timeout);
                    if(retval < WAIT_ABANDONED) //写锁时获得了所有互斥量,无需保存下标
                        lockSuccess = TRUE;
                }
            }
            ~RWLock()
            {
                if(NULL == _pObj)
                    return;
                if(readLockHandleIndex > -1)
                    _pObj->ReadUnlock(readLockHandleIndex);
                else
                    _pObj->WriteUnlock();
            }
            bool IsLockSuccess() const { return lockSuccess; }
        };

        class MyClass2: public RWLockBase<>
        {};
        MyClass2 mc2;

        void Say(char* text, int index)
        {
            static int count = 0;
            SYSTEMTIME st;
            ::GetLocalTime(&st);
            printf("%03d [%02d:%02d:%02d.%03d]%s %d/n", ++count, st.wHour, st.wMinute, st.wSecond, st.wMilliseconds, text, index);
        }

        //读线程
        DWORD CALLBACK ReadThreadProc(LPVOID param)
        {
            int i = (int)param;
            RWLock lock(&mc2);          //加读锁
            if(lock.IsLockSuccess())             //如果加锁成功
            {
                Say("read thread %d started", i);   //为了代码短一些,假设Say函数有这种能力
                Sleep(1000);
                Say("read thread %d ended", i);
            }
            else                                     //加锁超时,则显示超时信息
            {
                Say("read thread %d timeout", i);
            }
            return 0;
        }

        //写线程
        DWORD CALLBACK WriteThreadProc(LPVOID param)
        {
            int i = (int)param;
            RWLock lock(&mc2, false); //加写锁。
            if(lock.IsLockSuccess())
            {
                Say("write thread %d started", i);
                Sleep(600);
                Say("write thread %d ended", i);
            }
            else
            {
                Say("write thread %d timeout", i);
            }
            return 0;
        }


        int _tmain(int argc, _TCHAR* argv[])
        {
            int i;
            for(i = 0; i < 5; i++)
                ::CreateThread(0, 0, ReadThreadProc, (LPVOID)i, 0, 0);
            for(i = 0; i < 5; i++)
                ::CreateThread(0, 0, WriteThreadProc, (LPVOID)i, 0, 0);

            Sleep(10000);

            return 0;
        }

    三:Seamphore

    1:Semaphore是一件可以容纳N人的房间,如果人不满就可以进去,如果人满了,就要等待有人出来。对于N=1的情况,称为binary semaphore。一般的用法是,用于限制对于某一资源的同时访问。而Mutex是一把钥匙,一个人拿了就可进入一个房间,出来的时候把钥匙交给队列的 第一个。一般的用法是用于串行化对critical section代码的访问,保证这段代码不会被并行的运行。
    和Mutex的区别
    1)信号量没有所谓的”wait abandoned”状态可被其它线程侦测到;
    2)拥有互斥器的线程无论再调用多少次wait…()函数都不会被阻塞。但对于信号量,如果锁定成功也不会收到信号量的拥有权——因为同时可以多个线程 同时锁定一个信号量。信号量没有“独占锁定”这种事情,也没有所有权的观念,一个线程可以反复调用wait…()函数产生新的锁定,每锁定一次,信号量的 现值就减1。
    3)与互斥器不同,调用ReleaseSemaphore()的那个线程,并不一定就得是调用Wait…()函数的那个线程。任何线程都可以在任何时间调用ReleaseSemaphore(),解除被任何线程锁定的信号量。

    2:常用函数
        HANDLE CreateSemaphore();
        HANDLE OpenSemaphore();
        DWORD WaitForSingleObject();
        DWORD WaitForMultipleObjects();
        DWORD MsgWaitForMultipleObjects();
        BOOL ReleaseSemaphore();
        BOOL CloseHandle();

        HANDLE CreateSemaphore(LPSECURITY_ATTRIBUTES lpSemaphoreAttributes, //CE不支持
                               LONG lInitialCount, //信号量初始化计数值
                               LONG lMaximumCount, //信号量计数最大值,统一时间能锁定Seamphore之线程的最大个数。
                               LPCTSTR lpName); //信号量对象名称
                              
        BOOL ReleaseSemaphore(HANDLE hSemaphore, //信号量句柄
                              LONG lReleaseCount, //信号量计数增加的值
                              LPLONG lpPreviousCount); //输出量,表示上一次信号量计数
                         
    3:下列可以理解为有12个人需要使用10个房间 ,当时一个房间同一时刻只能被一个人使用。
        #define MAX_SEM_COUNT 10
        #define THREADCOUNT 12

        HANDLE ghSemaphore;

        DWORD WINAPI ThreadProc( LPVOID );

        void main()
        {
            HANDLE aThread[THREADCOUNT];
            DWORD ThreadID;
            int i;

            ghSemaphore = CreateSemaphore( NULL, MAX_SEM_COUNT, MAX_SEM_COUNT, NULL);
            if (ghSemaphore == NULL)
            {
                printf("CreateSemaphore error: %d/n", GetLastError());
                return;
            }

            for( i=0; i < THREADCOUNT; i++ )
            {
                aThread[i] = CreateThread(NULL, 0, (LPTHREAD_START_ROUTINE) ThreadProc, NULL, 0, &ThreadID);
                if( aThread[i] == NULL )
                {
                    printf("CreateThread error: %d/n", GetLastError());
                    return;
                }
            }

            WaitForMultipleObjects(THREADCOUNT, aThread, TRUE, INFINITE);
            for( i=0; i < THREADCOUNT; i++ )
                CloseHandle(aThread[i]);

            CloseHandle(ghSemaphore);
        }

        DWORD WINAPI ThreadProc( LPVOID lpParam )
        {
            DWORD dwWaitResult;
            BOOL bContinue=TRUE;

            while(bContinue)
            {
                dwWaitResult = WaitForSingleObject( ghSemaphore, 0L);

                switch (dwWaitResult)
                {
                case WAIT_OBJECT_0:
                    printf("Thread %d: wait succeeded/n", GetCurrentThreadId());
                    bContinue=FALSE;           
                    Sleep(5);
                    if (!ReleaseSemaphore(ghSemaphore,  1, NULL) ) 
                    {
                        printf("ReleaseSemaphore error: %d/n", GetLastError());
                    }
                    break;
                case WAIT_TIMEOUT:
                    printf("Thread %d: wait timed out/n", GetCurrentThreadId());
                    break;
                }
            }
            return TRUE;
        }

    4:对列Queue的同步访问。即生产者消费者概念。
        class Queue
        {
        public:
            Queue();
            ~Queue();
            void push(const int* t, size_t count);
            int pop();
        private:
            HANDLE semaphore;
            queue<int> queue;
        };

        //为了保证能顺利释放信号变量,初始队列中没有产品
        Queue:Queue() : semaphore(NULL)
          {
              semaphore = CreateSemaphore(NULL,0,1024*1024*10,NULL);   
          }

          Queue::~Queue()
          {
              if (semaphore)
                  CloseHandle(semaphore);   
          }
          //放入了Count了个产品后,Seamphore信号量重置值
          void Queue::push(const int* t, size_t count)
          {
              for (size_t i=0; i<count; i++)
              {
                  queue_.push(t[i]);
              }       
              ReleaseSemaphore(semaphore_, count, NULL);
          }

          int Queue::pop()
          {
              WaitForSingleObject(semaphore_, INFINITE);   
              return queue_.pop();   
          }

    四:Event

    1:
    如果一个事件是自动事件,那么当它处于激发状态时,可唤醒一个等待它的线程,线程被唤醒后,自动地转入非激发状态;
    如果一个事件是手动事件,那么当它处于激发状态时,可唤醒所有等待它的线程,并且一直保持状态为激发状态,直到被明确地ResetEvent()后,才转入非激发状态。

    2:常用函数
        HANDLE CreateEvent();
        HANDLE OpenEvent();
        BOOL SetEvent();
        BOOL PluseEvent();
        BOOL ResetEvent();
        DWORD WaitForSingleObject();
        DWORD WaitForMultipleObjects();
        DWORD MsgWaitForMultipleObjects();
        BOOL CloseHandle();
        HANDLE CreateEvent(LPSECURITY_ATTRIBUTES lpEventAttributes,   // 安全属性
                           BOOL bManualReset,   // 复位方式
                           BOOL bInitialState,   // 初始状态
                           LPCTSTR lpName);   // 对象名称

        bManualReset:指定将事件对象创建成手动复原还是自动复原。如果是TRUE,那么必须用ResetEvent函数来手工将事件的状态复原到无信号状态。如果设置为FALSE,当事件被一个等待线程释放以后,系统将会自动将事件状态复原为无信号状态。
        bInitialState:指定事件对象的初始状态。如果为TRUE,初始状态为有信号状态;否则为无信号状态。
        lpName:指定事件的对象的名称,任何线程或进程都可以根据这个名称使用这一Event对象。

    3:以下通过事件控制当写时,四个读线程不可操作队列。
        #define THREADCOUNT 4

        HANDLE ghWriteEvent;
        HANDLE ghThreads[THREADCOUNT];

        DWORD WINAPI ThreadProc(LPVOID);

        void CreateEventsAndThreads(void)
        {
          int i;
          DWORD dwThreadID;
          //自动重置事件
          ghWriteEvent = CreateEvent(NULL,TRUE,FALSE,TEXT("WriteEvent"));
          if (ghWriteEvent == NULL)
          {
              printf("CreateEvent failed (%d)/n", GetLastError());
              return;
          }

          for(i = 0; i < THREADCOUNT; i++)
          {
              ghThreads[i] = CreateThread(NULL,  0,   ThreadProc, NULL, 0, &dwThreadID);
              if (ghThreads[i] == NULL)
              {
                  printf("CreateThread failed (%d)/n", GetLastError());
                  return;
              }
          }
        }

        void WriteToBuffer(VOID)
        {
          printf("Main thread writing to the shared buffer.../n");
          if (! SetEvent(ghWriteEvent) )
          {
              printf("SetEvent failed (%d)/n", GetLastError());
              return;
          }
        }

        void CloseEvents()
        {
          CloseHandle(ghWriteEvent);
        }

        void main()
        {
          DWORD dwWaitResult;

          CreateEventsAndThreads();
          //开始写数据,写完后激活事件
          WriteToBuffer();

          printf("Main thread waiting for threads to exit.../n");

          //四个线程都在等待激活事件
          dwWaitResult = WaitForMultipleObjects(THREADCOUNT,   ghThreads,  TRUE, INFINITE);

          switch (dwWaitResult)
          {
          case WAIT_OBJECT_0:
              printf("All threads ended, cleaning up for application exit.../n");
              break;

          default:
              printf("WaitForMultipleObjects failed (%d)/n", GetLastError());
              return;
          }

          // Close the events to clean up

          CloseEvents();
        }

        DWORD WINAPI ThreadProc(LPVOID lpParam)
        {
          DWORD dwWaitResult;

          printf("Thread %d waiting for write event.../n", GetCurrentThreadId());

          dwWaitResult = WaitForSingleObject(  ghWriteEvent, INFINITE);  

          switch (dwWaitResult)
          {
          case WAIT_OBJECT_0:
              printf("Thread %d reading from buffer/n",GetCurrentThreadId());
              break;

          default:
              printf("Wait error (%d)/n", GetLastError());
              return 0;
          }

          printf("Thread %d exiting/n", GetCurrentThreadId());
          return 1;
        }

    五:Interlocked Variables
    interlocked函数没有“等待”机能,它只是保证对某个特定的变量的存取是排他性的、原子性的。这相当于给某个变量设置了一个临界区或互斥量。

    1:常用函数
        LONG InterlockedIncrement(LPLONG lpAddend);     //(*lpAddend)++;
        LONG InterlockedDecrement(LPLONG lpAddend);    //(*lpAddend)--;
        LONG InterlockedExchangeAdd(LPLONG Addend,LONG Increment);     //(*lpAddend) += Increment;
        LONG InterlockedExchange(LPLONG Target,LONG Value);     //(*lpAddend) = Value;
        PVOID InterlockedCompareExchange(PVOID *Destination,PVOID Exchange,PVOID Comperand);//if (*Destination == Comperand) *Destination = Exchange;

    六:总结
    1:Critical Section
        Critical section(临界区)用来实现“排他性占有”。适用范围是单一进程的各线程。它是:
        •    一个局部对象,不是核心对象;
        •    快速而有效率;
        •    不能够同时有一个以上的临界区被等待,否则容易陷入死锁;
        •    无法侦测是否被某个线程放弃。

    2:Mutex
        Mutex是一个核心对象,可以在不同的线程之间实现“排他性占有”,甚至那些线程分属不同的进程。它是:
        •    一个核心对象;
        •    如果mutex拥有的那个线程结束,则会产生一个”abandoned”错误信息;
        •    可以使用wait…()函数等待一个mutex;
        •    可以具名,因此可以被其它进程开启;
        •    只能被拥有它的线程释放。

    3:Semaphore
        Semaphore用来追踪有限的资源。它是:
        •    一个核心对象;
        •    没有拥有者;
        •    可以具名,因此可以被其它进程开启;
        •    可以被任何一个线程释放。

    4:Event Object
        Event object通常用于overlapped I/O,或用来设计某些自定义的同步对象。它是:
        •    一个核心对象;
        •    完全在程序掌控之下;
        •    适用于设计新的同步对象;
        •    “要求苏醒”的请求并不会被存储起来,可能会遗失掉;
        •    可以具名,因此可以被其它进程开启;

    5:Interlocked Variable
        Interlocked variable主要用于应用计数,不使用临界区或互斥器之类,对对4字节的数值操作有些基本的同步。

  • 相关阅读:
    Jenkins系列——使用SonarQube进行代码质量检查
    HTTP1.0工作原理
    Jenkins系列——使用checkstyle进行代码规范检查
    Jenkins系列——定时构建
    Hadoop环境搭建
    eclipse3.4+对的处理插件(附SVN插件安装实例)
    MD5
    RedHat6.5更新软件源
    ubuntu软件推荐
    disconf系列【2】——解决zk部署情况为空的问题
  • 原文地址:https://www.cnblogs.com/lzhitian/p/2791497.html
Copyright © 2011-2022 走看看