精华内容
下载资源
问答
  • 互斥

    2019-10-01 03:01:26
    通过互斥量我们可以指定资源被独占的方式使用,但如果有下面一种情况通过互斥量就无法处理,比如现在一位用户购买了一份三个并发访问许可的数据库系统,你的老板会要求你根据用户购买的访问许可数量来决定有多少个...

    补充理解:

       什么是“跨进程”,为什么关键区域不能“跨进程”?

    我们可以把环境(window/linux)必成海洋,每一个程序都是一只章鱼,章鱼离开海洋就是死的,动不了(如不能游动),丢到海洋里面,执行了,活了,就是进程。每一只章鱼都有很多爪子,有的用来抓东西吃,有的用来牵手----进程中的线程就好比章鱼的爪子。

      Mutex 作为内核对象,它可以明确指出 拥有者和创建者、下面是msdn的关于进程见的互斥:

    from   the   documentation:  
       
      http://msdn.microsoft.com/library/default.asp?url=/library/en-us/cpref/html/frlrfSystemThreadingMutexClassctorTopic.asp  
       
      //   This   example   shows   how   a   named   mutex   is   used   to   signal   between  
      //   processes   or   threads.  
       
       
                      bool   requestInitialOwnership   =   true;  
                      bool   mutexWasCreated;  
       
                      Mutex   m   =   new   Mutex(requestInitialOwnership,    
                                                              "MyMutex",    
                                                              out   mutexWasCreated);  
                       
                      //   This   thread   owns   the   mutex   only   if   it   both   requested    
                      //   initial   ownership   and   created   the   named   mutex.   Otherwise,  
                      //   it   can   request   the   named   mutex   by   calling   WaitOne.  
                      if   (!(requestInitialOwnership   &&   mutexWasCreated))  
                      {  
                              Console.WriteLine("Waiting   for   the   named   mutex.");  
                              m.WaitOne();  
                      }  
       
                      //   Once   the   process   has   gained   control   of   the   named   mutex,  
                      //   hold   onto   it   until   the   user   presses   ENTER.  
                      Console.WriteLine("This   process   owns   the   named   mutex.   "   +  
                              "Press   ENTER   to   release   the   mutex   and   exit.");  
                      Console.ReadLine();  
       
                      //   Call   ReleaseMutex   to   allow   other   threads   to   gain   control  
                      //   of   the   named   mutex.   If   you   keep   a   reference   to   the   local  
                      //   Mutex,   you   can   call   WaitOne   to   request   control   of   the    
                      //   named   mutex.  
                      m.ReleaseMutex();  

     

    基于内核

    //SetEvent.如果时间片越小,那么通过人工设置的防止发生错位的情况越严重

    SetEvent --设置信号量

    ReSetEvent 取消信号量

     

    本质:当人工重置时间对象得到通知的时候,等待该事件对象的所有线程都是能参与调度的线程

            当为自动重置时间对象的情况,则所有的线程中只有1个线程变成可调度,并且在得到线程的第一瞬间将线程变为无信号状态。

     

    什么情况下需要用Event呢?时间切片比较大。且使用比较自由

     

     

    关键区域:通过锁定一块分配的内存区,我们可以把它理解成一把锁。

    InitialzeCriticalSection(CRITICAL_SECTION)

    EnterCriticalSection(CRITICAL_SECTION &cs);

     

     

    区别:

     

    criticalsection不是内核对象,判断是否是内核对象的简单方法是看创建这个对象时是否需要指定安全描述符

     

    临界区不是内核对象,只可用在进程内部多个线程之间同步,不可用在进程间同步

     

     

     

     

    接下来要讲互斥量与临界区的作用非常相似,但互斥量是可以命名的,也就是说它可以跨越进程使用。所以创建互斥量需要的资源更多,所以如果只为了在进程内部是用的话使用临界区会带来速度上的优势并能够减少资源占用量。因为互斥量是跨进程的互斥量一旦被创建,就可以通过名字打开它。下面介绍可以用在互斥量上的API函数:

    创建互斥量:
    HANDLE CreateMutex(
      LPSECURITY_ATTRIBUTES lpMutexAttributes,// 安全信息
      BOOL bInitialOwner,  // 最初状态,
      //如果设置为真,则表示创建它的线程直接拥有了该互斥量,而不需要再申请
      LPCTSTR lpName       // 名字,可以为NULL,但这样一来就不能被其他线程/进程打开
    );
    打开一个存在的互斥量:
    HANDLE OpenMutex(
      DWORD dwDesiredAccess,  // 存取方式
      BOOL bInheritHandle,    // 是否可以被继承
      LPCTSTR lpName          // 名字
    );
    释放互斥量的使用权,但要求调用该函数的线程拥有该互斥量的使用权:
    BOOL ReleaseMutex(//作用如同LeaveCriticalSection
      HANDLE hMutex   // 句柄
    );
    关闭互斥量:
    BOOL CloseHandle(
      HANDLE hObject   // 句柄
    );
    

    你会说为什么没有名称如同EnterMutex,功能如同EnterCriticalSection一样的函数来获得互斥量的使用权呢?的确没有!获取互斥量的使用权需要使用函数:

    DWORD WaitForSingleObject(
      HANDLE hHandle,        // 等待的对象的句柄
      DWORD dwMilliseconds   // 等待的时间,以ms为单位,如果为INFINITE表示无限期的等待
    );
    返回:
    WAIT_ABANDONED 在等待的对象为互斥量时表明因为互斥量被关闭而变为有信号状态
    WAIT_OBJECT_0 得到使用权
    WAIT_TIMEOUT 超过(dwMilliseconds)规定时间
    

    在线程调用WaitForSingleObject后,如果一直无法得到控制权线程讲被挂起,直到超过时间或是获得控制权

     

     

    CRITICAL不是的,其实的都是从event继承过来的,event是最小的内核对象,其他内核对象的头部都有一个event.

     

    LeaveCriticalSection(CRITICAL_SECTION &cs); 

    DeleteCriticalSection(LRITICAL_SECTION &cs); //delete cs

    通过这组函数的配套来达到互斥的目的,应用很方便,但是如果多个互斥体的话,很容易造成死锁。

     

     

     

     

    通过互斥量我们可以指定资源被独占的方式使用,但如果有下面一种情况通过互斥量就无法处理,比如现在一位用户购买了一份三个并发访问许可的数据库系统,你的老板会要求你根据用户购买的访问许可数量来决定有多少个线程/进程能同时进行数据库操作,这时候如果利用互斥量就没有办法完成这个要求,信号灯对象可以说是一种资源计数器。对信号灯的操作伪代码大致如下:

    Semaphore sem=3;
    
    dword threadA(void*)
    {
    	while(sem <= 0)
    	{// 相当于 WaitForSingleObject
    		wait ...
    	}
    	// sem > 0
    	// lock the Semaphore
    	sem -- ;
    	do functions ...
    	// release Semaphore
    	sem ++ ;
    	return 0;
    }
    

    这里信号灯有一个初始值,表示有多少进程/线程可以进入,当信号灯的值大于0时为有信号状态,小于等于0时为无信号状态,所以可以利用WaitForSingleObject进行等待,当WaitForSingleObject等待成功后信号灯的值会被减少1,直到释放时信号灯会被增加1。用于信号灯操作的API函数有下面这些:

    创建信号灯:
    HANDLE CreateSemaphore(
      LPSECURITY_ATTRIBUTES lpSemaphoreAttributes,// 安全属性,NULL表示使用默认的安全描述
      LONG lInitialCount,  // 初始值
      LONG lMaximumCount,  // 最大值
      LPCTSTR lpName       // 名字
    );
    打开信号灯:
    HANDLE OpenSemaphore(
      DWORD dwDesiredAccess,  // 存取方式
      BOOL bInheritHandle,    // 是否能被继承
      LPCTSTR lpName          // 名字
    );
    释放信号灯:
    BOOL ReleaseSemaphore(
      HANDLE hSemaphore,   // 句柄
      LONG lReleaseCount,  // 释放数,让信号灯值增加数
      LPLONG lpPreviousCount   // 用来得到释放前信号灯的值,可以为NULL
    );
    关闭信号灯:
    BOOL CloseHandle(
      HANDLE hObject   // 句柄
    );
    

    可以看出来信号灯的使用方式和互斥量的使用方式非常相似,下面的代码使用初始值为2的信号灯来保证只有两个线程可以同时进行数据库调用:

    DWORD threadA(void* pD)
    {
    	int iID=(int)pD;
    	//在内部重新打开
    	HANDLE hCounterIn=OpenSemaphore(SEMAPHORE_ALL_ACCESS,FALSE,"sam sp 44");
    
    	for(int i=0;i<3;i++)
    	{
    		printf("%d wait for object\n",iID);
    		WaitForSingleObject(hCounterIn,INFINITE);
    		printf("\t\tthread %d : do database access call\n",iID);
    		Sleep(100);
    		printf("\t\tthread %d : do database access call end\n",iID);
    		ReleaseSemaphore(hCounterIn,1,NULL);
    	}
    	CloseHandle(hCounterIn);
    	return 0;
    }
    

     

     

     

     

    ///good article

    LOGO1
    您当前位置 首页 开发教程 Visual C++/MFC专题讲座 4.4 进程/线程间同步

    4.4 进程/线程间同步

    这一节的内容比较多请你耐心的看完,因为进程/线程间同步的方法比较多,每种方法都有不同的用途:这节中会讲通过临界区互斥量信号灯事件来进行同步。

    由于进程/线程间的操作是并行进行的,所以就产生了一个数据的问题同步,我们先看一段代码:

    int iCounter=0;//全局变量
    DOWRD threadA(void* pD)
    {
    	for(int i=0;i<100;i++)
    	{
    		int iCopy=iCounter;
    		//Sleep(1000);
    		iCopy++;
    		//Sleep(1000);
    		iCounter=iCopy;
    	}
    }
    
    现在假设有两个线程threadA1和threadA2在同时运行那么运行结束后iCounter的值会是多少,是200吗?不是的,如果我们将Sleep(1000)前的注释去掉后我们会很容易明白这个问题,因为在iCounter的值被正确修改前它可能已经被其他的线程修改了。这个例子是一个将机器代码操作放大的例子,因为在CPU内部也会经历数据读/写的过程,而在线程执行的过程中线程可能被中断而让其他线程执行。变量iCounter在被第一个线程修改后,写回内存前如果它又被第二个线程读取,然后才被第一个线程写回,那么第二个线程读取的其实是错误的数据,这种情况就称为脏读(dirty read)。这个例子同样可以推广到对文件,资源的使用上。

    那么要如何才能避免这一问题呢,假设我们在使用iCounter前向其他线程询问一下:有谁在用吗?如果没被使用则可以立即对该变量进行操作,否则等其他线程使用完后再使用,而且在自己得到该变量的控制权后其他线程将不能使用这一变量,直到自己也使用完并释放为止。经过修改的伪代码如下:

    int iCounter=0;//全局变量
    DOWRD threadA(void* pD)
    {
    	for(int i=0;i<100;i++)
    	{
    		ask to lock iCounter
    		wait other thread release the lock
    		lock successful
    		
    		{
    			int iCopy=iCounter;
    			//Sleep(1000);
    			iCopy++;
    		}
    		iCounter=iCopy;
    		
    		release lock of iCounter
    	}
    }
    

    幸运的是OS提供了多种同步对象供我们使用,并且可以替我们管理同步对象的加锁和解锁。我们需要做的就是对每个需要同步使用的资源产生一个同步对象,在使用该资源前申请加锁,在使用完成后解锁。接下来我们介绍一些同步对象:

    临界区:临界区是一种最简单的同步对象,它只可以在同一进程内部使用。它的作用是保证只有一个线程可以申请到该对象,例如上面的例子我们就可以使用临界区来进行同步处理。几个相关的API函数为:

    • VOID InitializeCriticalSection(LPCRITICAL_SECTION lpCriticalSection );产生临界区
    • VOID DeleteCriticalSection(LPCRITICAL_SECTION lpCriticalSection );删除临界区
    • VOID EnterCriticalSection(LPCRITICAL_SECTION lpCriticalSection );进入临界区,相当于申请加锁,如果该临界区正被其他线程使用则该函数会等待到其他线程释放
    • BOOL TryEnterCriticalSection(LPCRITICAL_SECTION lpCriticalSection );进入临界区,相当于申请加锁,和EnterCriticalSection不同如果该临界区正被其他线程使用则该函数会立即返回FALSE,而不会等待
    • VOID LeaveCriticalSection(LPCRITICAL_SECTION lpCriticalSection );退出临界区,相当于申请解锁

    下面的示范代码演示了如何使用临界区来进行数据同步处理:

    //全局变量
    int iCounter=0;
    CRITICAL_SECTION criCounter;
    
    DWORD threadA(void* pD)
    {
    	int iID=(int)pD;
    	for(int i=0;i<8;i++)
    	{
    		EnterCriticalSection(&criCounter);
    		int iCopy=iCounter;
    		Sleep(100);
    		iCounter=iCopy+1;
    		printf("thread %d : %d\n",iID,iCounter);
    		LeaveCriticalSection(&criCounter);
    	}
    	return 0;
    }
    //in main function
    {
    		//创建临界区
    		InitializeCriticalSection(&criCounter);
    		//创建线程
    		HANDLE hThread[3];
    		CWinThread* pT1=AfxBeginThread((AFX_THREADPROC)threadA,(void*)1);
    		CWinThread* pT2=AfxBeginThread((AFX_THREADPROC)threadA,(void*)2);
    		CWinThread* pT3=AfxBeginThread((AFX_THREADPROC)threadA,(void*)3);
    		hThread[0]=pT1->m_hThread;
    		hThread[1]=pT2->m_hThread;
    		hThread[2]=pT3->m_hThread;
    		//等待线程结束
    		//至于WaitForMultipleObjects的用法后面会讲到。
    		WaitForMultipleObjects(3,hThread,TRUE,INFINITE);
    		//删除临界区
    		DeleteCriticalSection(&criCounter);
    		printf("\nover\n");
    
    }
    

    接下来要讲互斥量与临界区的作用非常相似,但互斥量是可以命名的,也就是说它可以跨越进程使用。所以创建互斥量需要的资源更多,所以如果只为了在进程内部是用的话使用临界区会带来速度上的优势并能够减少资源占用量。因为互斥量是跨进程的互斥量一旦被创建,就可以通过名字打开它。下面介绍可以用在互斥量上的API函数:

    创建互斥量:
    HANDLE CreateMutex(
      LPSECURITY_ATTRIBUTES lpMutexAttributes,// 安全信息
      BOOL bInitialOwner,  // 最初状态,
      //如果设置为真,则表示创建它的线程直接拥有了该互斥量,而不需要再申请
      LPCTSTR lpName       // 名字,可以为NULL,但这样一来就不能被其他线程/进程打开
    );
    打开一个存在的互斥量:
    HANDLE OpenMutex(
      DWORD dwDesiredAccess,  // 存取方式
      BOOL bInheritHandle,    // 是否可以被继承
      LPCTSTR lpName          // 名字
    );
    释放互斥量的使用权,但要求调用该函数的线程拥有该互斥量的使用权:
    BOOL ReleaseMutex(//作用如同LeaveCriticalSection
      HANDLE hMutex   // 句柄
    );
    关闭互斥量:
    BOOL CloseHandle(
      HANDLE hObject   // 句柄
    );
    

    你会说为什么没有名称如同EnterMutex,功能如同EnterCriticalSection一样的函数来获得互斥量的使用权呢?的确没有!获取互斥量的使用权需要使用函数:

    DWORD WaitForSingleObject(
      HANDLE hHandle,        // 等待的对象的句柄
      DWORD dwMilliseconds   // 等待的时间,以ms为单位,如果为INFINITE表示无限期的等待
    );
    返回:
    WAIT_ABANDONED 在等待的对象为互斥量时表明因为互斥量被关闭而变为有信号状态
    WAIT_OBJECT_0 得到使用权
    WAIT_TIMEOUT 超过(dwMilliseconds)规定时间
    

    在线程调用WaitForSingleObject后,如果一直无法得到控制权线程讲被挂起,直到超过时间或是获得控制权。

    讲到这里我们必须更深入的讲一下WaitForSingleObject函数中的对象(Object)的含义,这里的对象是一个具有信号状态的对象,对象有两种状态:有信号/无信号。而等待的含义就在于等待对象变为有信号的状态,对于互斥量来讲如果正在被使用则为无信号状态,被释放后变为有信号状态。当等待成功后WaitForSingleObject函数会将互斥量置为无信号状态,这样其他的线程就不能获得使用权而需要继续等待。WaitForSingleObject函数还进行排队功能,保证先提出等待请求的线程先获得对象的使用权,下面的代码演示了如何使用互斥量来进行同步,代码的功能还是进行全局变量递增,通过输出结果可以看出,先提出请求的线程先获得了控制权:

    int iCounter=0;
    
    DWORD threadA(void* pD)
    {
    	int iID=(int)pD;
    	//在内部重新打开
    	HANDLE hCounterIn=OpenMutex(MUTEX_ALL_ACCESS,FALSE,"sam sp 44");
    
    	for(int i=0;i<8;i++)
    	{
    		printf("%d wait for object\n",iID);
    		WaitForSingleObject(hCounterIn,INFINITE);
    		int iCopy=iCounter;
    		Sleep(100);
    		iCounter=iCopy+1;
    		printf("\t\tthread %d : %d\n",iID,iCounter);
    		ReleaseMutex(hCounterIn);
    	}
    	CloseHandle(hCounterIn);
    	return 0;
    }
    
    //in main function
    {
    		//创建互斥量
    		HANDLE hCounter=NULL;
    		if( (hCounter=OpenMutex(MUTEX_ALL_ACCESS,FALSE,"sam sp 44"))==NULL)
    		{
    			//如果没有其他进程创建这个互斥量,则重新创建
    			hCounter = CreateMutex(NULL,FALSE,"sam sp 44");
    		}
    
    		//创建线程
    		HANDLE hThread[3];
    		CWinThread* pT1=AfxBeginThread((AFX_THREADPROC)threadA,(void*)1);
    		CWinThread* pT2=AfxBeginThread((AFX_THREADPROC)threadA,(void*)2);
    		CWinThread* pT3=AfxBeginThread((AFX_THREADPROC)threadA,(void*)3);
    		hThread[0]=pT1->m_hThread;
    		hThread[1]=pT2->m_hThread;
    		hThread[2]=pT3->m_hThread;
    		//等待线程结束
    		WaitForMultipleObjects(3,hThread,TRUE,INFINITE);
    
    		//关闭句柄
    		CloseHandle(hCounter);
    	}
    }
    

    在这里我没有使用全局变量来保存互斥量句柄,这并不是因为不能这样做,而是为演示如何在其他的代码段中通过名字来打开已经创建的互斥量。其实这个例子在逻辑上是有一点错误的,因为iCounter这个变量没有跨进程使用,所以没有必要使用互斥量,只需要使用临界区就可以了。假设有一组进程在同时使用一个文件那么我们可以使用互斥量来保证该文件只同时被一个进程使用(如果只是利用OS的文件存取控制功能则需要添加更多的错误处理代码),此外在调度程序中也可以使用互斥量来对资源的使用进行同步化。

    现在我们回过头来讲WaitForSingleObject这个函数,从前面的例子中我们看到WaitForSingleObject这个函数将等待一个对象变为有信号状态,那么具有信号状态的对象有哪些呢?下面是一部分:

    • Mutex
    • Event
    • Semaphore
    • Job
    • Process
    • Thread
    • Waitable timer
    • Console input

    互斥量(Mutex),信号灯(Semaphore),事件(Event)都可以被跨越进程使用来进行同步数据操作,而其他的对象与数据同步操作无关,但对于进程和线程来讲,如果进程和线程在运行状态则为无信号状态,在退出后为有信号状态。所以我们可以使用WaitForSingleObject来等待进程和线程退出。(至于信号灯,事件的用法我们接下来会讲)我们在前面的例子中使用了WaitForMultipleObjects函数,这个函数的作用与WaitForSingleObject类似但从名字上我们可以看出,WaitForMultipleObjects将用于等待多个对象变为有信号状态,函数原型如下:

    DWORD WaitForMultipleObjects(
      DWORD nCount,             // 等待的对象数量
      CONST HANDLE *lpHandles,  // 对象句柄数组指针
      BOOL fWaitAll,            // 等待方式,
      //为TRUE表示等待全部对象都变为有信号状态才返回,为FALSE表示任何一个对象变为有信号状态则返回
      DWORD dwMilliseconds      // 超时设置,以ms为单位,如果为INFINITE表示无限期的等待
    );
    

    返回值意义:
    WAIT_OBJECT_0 到 (WAIT_OBJECT_0 + nCount – 1):当fWaitAll为TRUE时表示所有对象变为有信号状态,当fWaitAll为FALSE时使用返回值减去WAIT_OBJECT_0得到变为有信号状态的对象在数组中的下标。
    WAIT_ABANDONED_0 到 (WAIT_ABANDONED_0 + nCount – 1):当fWaitAll为TRUE时表示所有对象变为有信号状态,当fWaitAll为FALSE时表示对象中有一个对象为互斥量,该互斥量因为被关闭而成为有信号状态,使用返回值减去WAIT_OBJECT_0得到变为有信号状态的对象在数组中的下标。
    WAIT_TIMEOUT:表示超过规定时间。

    前面的例子中的如下代码表示等待三个线程都变为有信号状态,也就是说三个线程都结束。
    		HANDLE hThread[3];
    		CWinThread* pT1=AfxBeginThread((AFX_THREADPROC)threadA,(void*)1);
    		CWinThread* pT2=AfxBeginThread((AFX_THREADPROC)threadA,(void*)2);
    		CWinThread* pT3=AfxBeginThread((AFX_THREADPROC)threadA,(void*)3);
    		hThread[0]=pT1->m_hThread;
    		hThread[1]=pT2->m_hThread;
    		hThread[2]=pT3->m_hThread;
    		//等待线程结束
    		WaitForMultipleObjects(3,hThread,TRUE,INFINITE);
    此外,在启动和等待进程结束一文中就利用这个功能等待进程结束。
    

    通过互斥量我们可以指定资源被独占的方式使用,但如果有下面一种情况通过互斥量就无法处理,比如现在一位用户购买了一份三个并发访问许可的数据库系统,你的老板会要求你根据用户购买的访问许可数量来决定有多少个线程/进程能同时进行数据库操作,这时候如果利用互斥量就没有办法完成这个要求,信号灯对象可以说是一种资源计数器。对信号灯的操作伪代码大致如下:

    Semaphore sem=3;
    
    dword threadA(void*)
    {
    	while(sem <= 0)
    	{// 相当于 WaitForSingleObject
    		wait ...
    	}
    	// sem > 0
    	// lock the Semaphore
    	sem -- ;
    	do functions ...
    	// release Semaphore
    	sem ++ ;
    	return 0;
    }
    

    这里信号灯有一个初始值,表示有多少进程/线程可以进入,当信号灯的值大于0时为有信号状态,小于等于0时为无信号状态,所以可以利用WaitForSingleObject进行等待,当WaitForSingleObject等待成功后信号灯的值会被减少1,直到释放时信号灯会被增加1。用于信号灯操作的API函数有下面这些:

    创建信号灯:
    HANDLE CreateSemaphore(
      LPSECURITY_ATTRIBUTES lpSemaphoreAttributes,// 安全属性,NULL表示使用默认的安全描述
      LONG lInitialCount,  // 初始值
      LONG lMaximumCount,  // 最大值
      LPCTSTR lpName       // 名字
    );
    打开信号灯:
    HANDLE OpenSemaphore(
      DWORD dwDesiredAccess,  // 存取方式
      BOOL bInheritHandle,    // 是否能被继承
      LPCTSTR lpName          // 名字
    );
    释放信号灯:
    BOOL ReleaseSemaphore(
      HANDLE hSemaphore,   // 句柄
      LONG lReleaseCount,  // 释放数,让信号灯值增加数
      LPLONG lpPreviousCount   // 用来得到释放前信号灯的值,可以为NULL
    );
    关闭信号灯:
    BOOL CloseHandle(
      HANDLE hObject   // 句柄
    );
    

    可以看出来信号灯的使用方式和互斥量的使用方式非常相似,下面的代码使用初始值为2的信号灯来保证只有两个线程可以同时进行数据库调用:

    DWORD threadA(void* pD)
    {
    	int iID=(int)pD;
    	//在内部重新打开
    	HANDLE hCounterIn=OpenSemaphore(SEMAPHORE_ALL_ACCESS,FALSE,"sam sp 44");
    
    	for(int i=0;i<3;i++)
    	{
    		printf("%d wait for object\n",iID);
    		WaitForSingleObject(hCounterIn,INFINITE);
    		printf("\t\tthread %d : do database access call\n",iID);
    		Sleep(100);
    		printf("\t\tthread %d : do database access call end\n",iID);
    		ReleaseSemaphore(hCounterIn,1,NULL);
    	}
    	CloseHandle(hCounterIn);
    	return 0;
    }
    //in main function
    {
    		//创建信号灯
    		HANDLE hCounter=NULL;
    		if( (hCounter=OpenSemaphore(SEMAPHORE_ALL_ACCESS,FALSE,"sam sp 44"))==NULL)
    		{
    			//如果没有其他进程创建这个信号灯,则重新创建
    			hCounter = CreateSemaphore(NULL,2,2,"sam sp 44");
    		}
    
    		//创建线程
    		HANDLE hThread[3];
    		CWinThread* pT1=AfxBeginThread((AFX_THREADPROC)threadA,(void*)1);
    		CWinThread* pT2=AfxBeginThread((AFX_THREADPROC)threadA,(void*)2);
    		CWinThread* pT3=AfxBeginThread((AFX_THREADPROC)threadA,(void*)3);
    		hThread[0]=pT1->m_hThread;
    		hThread[1]=pT2->m_hThread;
    		hThread[2]=pT3->m_hThread;
    		//等待线程结束
    		WaitForMultipleObjects(3,hThread,TRUE,INFINITE);
    
    		//关闭句柄
    		CloseHandle(hCounter);	
    }
    

    信号灯有时用来作为计数器使用,一般来讲将其初始值设置为0,先调用ReleaseSemaphore来增加其计数,然后使用WaitForSingleObject来减小其计数,遗憾的是通常我们都不能得到信号灯的当前值,但是可以通过设置WaitForSingleObject的等待时间为0来检查信号灯当前是否为0。

    接下来我们讲最后一种同步对象:事件,前面讲的信号灯和互斥量可以保证资源被正常的分配和使用,而事件是用来通知其他进程/线程某件操作已经完成。例如:现在有三个线程:threadA,threadB,threadC,现在要求他们中的部分功能要顺序执行,也就是说threadA执行完一部分后threadB执行,threadB执行完一部分后threadC开始执行。也许你觉得下面的代码可以满足要求:

    要求:A1执行完后执行B2然后执行C3,再假设每个任务的执行时间都为1,而且允许并发操作。
    方案一:
    dword threadA(void*)
    {
    	do something A1;
    	create threadB;
    	do something A2;
    	do something A3;
    }
    
    dword threadB(void*)
    {
    	do something B1;
    	do something B2;
    	create threadC;
    	do something B3;
    }
    
    dword threadC(void*)
    {
    	do something C1;
    	do something C2;
    	do something C3;
    }
    
    方案二:
    dword threadA(void*)
    {
    	do something A1;
    	do something A2;
    	do something A3;
    }
    
    dword threadB(void*)
    {
    	do something B1;
    	wait for threadA end
    	do something B2;
    	do something B3;
    }
    
    dword threadC(void*)
    {
    	do something C1;
    	do something C2;
    	wait for threadB end
    	do something C3;
    }
    
    main()
    {
    	create threadA;
    	create threadB;
    	create threadC;
    }
    
    方案三:
    dword threadA(void*)
    {
    	do something A1;
    	release event1;
    	do something A2;
    	do something A3;
    }
    
    dword threadB(void*)
    {
    	do something B1;
    	wait for envet1 be released
    	do something B2;
    	release event2;
    	do something B3;
    }
    
    dword threadC(void*)
    {
    	do something C1;
    	do something C2;
    	wait for event2 be released
    	do something C3;
    }
    
    main()
    {
    	create threadA;
    	create threadB;
    	create threadC;
    }
    比较一下三种方案的执行时间:
             方案一                        方案二                      方案三
    1 threadA  threadB  threadC    threadA  threadB  threadC   threadA  threadB  threadC
    2    A1                           A1        B1       C1       A1       B1       C1
    3    A2      B1                   A2                 C2       A2       B2       C2
    4    A1      B2                   A3                          A3       B3       C3
    5            B3         C1                  B2      
    6                       C2                  B3        
    7                       C3                           C3
    8                                                   
    

    可以看出来方案三的执行时间是最短的,当然这个例子有些极端,但我们可以看出事件对象用于通知其他进程/线程某件操作已经完成方面的作用是很大的,而且如果有的任务要在进程尖进行协调采用等待其他进程中线程结束的方式是不可能实现的。此外我也希望通过这个例子讲一点关于分析线程执行效率的方法。

    事件对象可以一两种方式创建,一种为自动重置,在其他线程使用WaitForSingleObject等待到事件对象变为有信号后该事件对象自动又变为无信号状态,一种为人工重置在其他线程使用WaitForSingleObject等待到事件对象变为有信号后该事件对象状态不变。例如有多个线程都在等待一个线程运行结束,我们就可以使用人工重置事件,在被等待的线程结束时设置该事件为有信号状态,这样其他的多个线程对该事件的等待都会成功(因为该事件的状态不会被自动重置)。事件相关的API如下:

    创建事件对象:
    HANDLE CreateEvent(
      LPSECURITY_ATTRIBUTES lpEventAttributes,// 安全属性,NULL表示使用默认的安全描述
      BOOL bManualReset,  // 是否为人工重置
      BOOL bInitialState, // 初始状态是否为有信号状态
      LPCTSTR lpName      // 名字
    );
    打开事件对象:
    HANDLE OpenEvent(
      DWORD dwDesiredAccess,  // 存取方式
      BOOL bInheritHandle,    // 是否能够被继承
      LPCTSTR lpName          // 名字
    );
    设置事件为无信号状态:
    BOOL ResetEvent(
      HANDLE hEvent   // 句柄
    );
    设置事件有无信号状态:
    BOOL SetEvent(
      HANDLE hEvent   // 句柄
    );
    关闭事件对象:
    BOOL CloseHandle(
      HANDLE hObject   // 句柄
    );
    

    下面的代码演示了自动重置和人工重置事件在使用中的不同效果:

    DWORD threadA(void* pD)
    {
    	int iID=(int)pD;
    	//在内部重新打开
    	HANDLE hCounterIn=OpenEvent(EVENT_ALL_ACCESS,FALSE,"sam sp 44");
    
    	printf("\tthread %d begin\n",iID);
    	//设置成为有信号状态
    	Sleep(1000);
    	SetEvent(hCounterIn);
    	Sleep(1000);
    	printf("\tthread %d end\n",iID);
    	CloseHandle(hCounterIn);
    	return 0;
    }
    
    DWORD threadB(void* pD)
    {//等待threadA结束后在继续执行
    	int iID=(int)pD;
    	//在内部重新打开
    	HANDLE hCounterIn=OpenEvent(EVENT_ALL_ACCESS,FALSE,"sam sp 44");
    
    	if(WAIT_TIMEOUT == WaitForSingleObject(hCounterIn,10*1000))
    	{
    		printf("\t\tthread %d wait time out\n",iID);
    	}
    	else
    	{
    		printf("\t\tthread %d wait ok\n",iID);
    	}
    	CloseHandle(hCounterIn);
    	return 0;
    }
    
    //in main function
    {
    		HANDLE hCounter=NULL;
    		if( (hCounter=OpenEvent(EVENT_ALL_ACCESS,FALSE,"sam sp 44"))==NULL)
    		{
    			//如果没有其他进程创建这个事件,则重新创建,该事件为人工重置事件
    			hCounter = CreateEvent(NULL,TRUE,FALSE,"sam sp 44");
    		}
    
    		//创建线程
    		HANDLE hThread[3];
    		printf("test of manual rest event\n");
    		CWinThread* pT1=AfxBeginThread((AFX_THREADPROC)threadA,(void*)1);
    		CWinThread* pT2=AfxBeginThread((AFX_THREADPROC)threadB,(void*)2);
    		CWinThread* pT3=AfxBeginThread((AFX_THREADPROC)threadB,(void*)3);
    		hThread[0]=pT1->m_hThread;
    		hThread[1]=pT2->m_hThread;
    		hThread[2]=pT3->m_hThread;
    		//等待线程结束
    		WaitForMultipleObjects(3,hThread,TRUE,INFINITE);
    		//关闭句柄
    		CloseHandle(hCounter);
    
    		if( (hCounter=OpenEvent(EVENT_ALL_ACCESS,FALSE,"sam sp 44"))==NULL)
    		{
    			//如果没有其他进程创建这个事件,则重新创建,该事件为自动重置事件
    			hCounter = CreateEvent(NULL,FALSE,FALSE,"sam sp 44");
    		}
    		//创建线程
    		printf("test of auto rest event\n");
    		pT1=AfxBeginThread((AFX_THREADPROC)threadA,(void*)1);
    		pT2=AfxBeginThread((AFX_THREADPROC)threadB,(void*)2);
    		pT3=AfxBeginThread((AFX_THREADPROC)threadB,(void*)3);
    		hThread[0]=pT1->m_hThread;
    		hThread[1]=pT2->m_hThread;
    		hThread[2]=pT3->m_hThread;
    		//等待线程结束
    		WaitForMultipleObjects(3,hThread,TRUE,INFINITE);
    		//关闭句柄
    		CloseHandle(hCounter);
    }	
    

    从执行结果中我们可以看到在第二次执行时由于使用了自动重置事件threadB中只有一个线程能够等待到threadA中释放的事件对象。

    在处理多进程/线程的同步问题时必须要小心避免发生死锁问题,比如说现在有两个互斥量A和B,两个线程tA和tB,他们在执行前都需要得到这两个互斥量,但现在这种情况发生了,tA拥有了互斥量A,tB拥有了互斥量B,但它们同时都在等待拥有另一个互斥量,这时候显然谁也不可能得到自己希望的资源。这种互相拥有对方所拥有的资源而且都在等待对方拥有的资源的情况就称为死锁。关于这个问题更详细的介绍请参考其他参考书。

    在MFC中对于各种同步对象都提供了相对应的类

    在这些类中封装了上面介绍的对象创建,打开,控制,删除功能。但是如果要使用等待功能则需要使用另外两个类:CSingleLock和CMultiLock。这两个类中封装了WaitForSingleObject和WaitForMultipleObjects函数。如果大家觉的需要可以看看这些类的定义,我想通过上面的介绍可以很容易理解,但是在对象同步问题上我觉得使用API函数比使用MFC类更为直观和方便。

    下载本节示范代码 25K

    最后我留一个问题给大家吧,这个问题是一个比较经典的同步问题:一个写/多个读,规则如下:

    1. 一个人在写时,其他人不允许写。
    2. 一个人在写时,其他人不允许读。
    3. 一个人在读时,其他人不允许写。
    4. 一个人在读时,其他人允许读。
    如果谁有兴趣在做完后可以将代码和方法邮寄给我在本站发表。

    返回

    版权所有 闻怡洋 http://www.vchelp.net/

     

    转载于:https://www.cnblogs.com/xianqingzh/archive/2009/11/30/1614160.html

    展开全文
  • Qt中QThread 互斥,事件,临界区,信号量

    千次阅读 2017-09-05 10:21:56
    含义就是判断d_ptr当前的值是不是0,如果是0的话,则将dummyLocked()的值赋给d_ptr,并返回真值;否则什么都不做,并返回false。 static inline QMutexData *dummyLocked() { return reinterpret_cast*>...

    在gemfield的《从pthread到QThread》一文中我们了解了线程的基本使用,但是有一大部分的内容当时说要放到这片文章里讨论,那就是线程的同步问题。关于这个问题,gemfield在《从进 程到线程》中有一个比喻,有必要重新放在下面温习下:

    *******************************
    最后用一个比喻来总结下:
    1、一个进程就好比一个房子里有一个人;
    2、clone创建线程就相当于在这个房子里添加一个人;
    3、fork创建进程就相当于再造一个房子,然后在新房子里添加一个人;

    有了上面的比喻后,我们就清楚很多了:
    1、线程之间有很多资源可以共享:比如厨房资源、洗手间资源、热水器资源等;
    2、而对于进程来说,一个概念就是进程间通信(你要和另外一个房子里的人通信要比一个房子里的两个人之间通信复杂);
    3、线程之间因为共享内存,所以通过一个全局的变量就可以交换数据了;
    4、但与此同时,对于线程来说,又有新的概念产生了:
    a、一个人使用洗手间的时候,得锁上以防止另一个人对洗手间的访问;
    b、一个人(或几个人)睡觉的时候,另外一个人可以按照之前约定的方式来叫醒他;
    c、热水器的电源要一直开着,直到想洗澡的人数减为0;

    上面的概念,在gemfield的后文中术语化的时候,你就不会再觉得很深奥或者枯燥了。
    ********************************
    对于上面的a:一个人使用洗手间的时候,得锁上以防止另一个人对洗手间的访问。我们在QThread里使用的就是QMutext这个互斥了。mutex是mutual exclusion(互相排斥)的简写。在pthread中也有pthread_mutex_*族,但是在QThread中我们能在Qt的框架下通过源代码看到具体实现,所以pthread_mutex_*就靠你自行研究了。

    第一部分、QMutex的研究

    1、来一小段代码背景:
    ************************
    int number = 6;

    void gemfield1()
    {
    number *= 5;
    number /= 4;
    }

    void gemfield2()
    {
    number *= 3;
    number /= 2;
    }
    **************************
    如果下面的代码是顺序执行的,则会有下面这样的输出逻辑:
    **************************
    // gemfield1()
    number *= 5; // number 为 30
    number /= 4; // number 为 7

    // gemfield2()
    number *= 3; // number 为 21
    number /= 2; // number 为 10
    **************************
    但如果是在2个线程中(线程1、线程2)分别同时调用了gemfield1()、gemfield2()呢?
    **************************
    // 线程1调用gemfield1()
    number *= 5; // number 为30

    // 线程2 调用 gemfield2().
    // 线程1 被系统调度出去了,而把线程2调度进来运行
    number *= 3; // number 为90
    number /= 2; // number 为45

    // 线程1 结束运行
    number /= 4; // number 为11, 而不是上面的10
    **************************

    2、如何解决这个问题?

    很明显我们想要一个线程(比如线程1)在访问变量number的时候,除非该线程(比如线程1)允许,否则其他线程(比如线程2)不能访问number;这就好比一个人访问洗手间,另一个人就无法访问一样(我们把对number的访问区域,或者洗手间这个区域称作临界区域);下面就是QMutex的使用:
    ***************************
    QMutex mutex;
    int number = 6;

    void gemfield1()
    {
    mutex.lock();
    number *= 5;
    number /= 4;
    mutex.unlock();
    }

    void gemfield2()
    {
    mutex.lock();
    number *= 3;
    number /= 2;
    mutex.unlock();
    }
    ****************************
    当mutex这个互斥lock上之后,直到unlock之前,都只有1个线程访问number;注意:mutex变量和number一样是全局变量!

    在QMutex的使用中,我们关注以下4个方法和2个属性:

    1、QMutex ()//构造1个mutex
    2、lock ()//锁
    3、tryLock ()//尝试着锁
    4、unlock ()//释放锁

    另外两个属性是:递归和非递归。如果这个mutex是递归的话,表明它可以被一个线程锁多次,也就是锁和解锁中再嵌套锁和解锁;非递归的话,就表明mutex只能被锁一次。

    这四个的用法已经在上面的代码中展示过了,现在来看看QMutex是怎么做到这一点的?

    3、QMutex是如何做到保护临界区域的?

    设想一下我们的洗手间问题:洗手间提供了什么机制,让一个人在使用的时候,另一个人无法闯入?门锁!现在开始我们的QMutex之旅:

    a、首先得构造出一个QMutex对象吧,要了解这一点,我们得先了解下QMutex的类型层次及成员。

    class QBasicMutex
    {
    public:
    inline void lock() {
    if (!fastTryLock())
    lockInternal();
    }

    inline void unlock() {
    Q_ASSERT(d_ptr.load()); //mutex must be locked
    if (!d_ptr.testAndSetRelease(dummyLocked(), 0))
    unlockInternal();
    }

    bool tryLock(int timeout = 0) {
    return fastTryLock() || lockInternal(timeout);
    }

    private:
    inline bool fastTryLock() {
    return d_ptr.testAndSetAcquire(0, dummyLocked());
    }
    bool lockInternal(int timeout = -1);
    void unlockInternal();
    QBasicAtomicPointer<QMutexData> d_ptr;

    static inline QMutexData *dummyLocked() {
    return reinterpret_cast<QMutexData *>(quintptr(1));
    }

    friend class QMutex;
    friend class QMutexData;
    };
    ————————————————–
    class QMutex : public QBasicMutex {
    public:
    enum RecursionMode { NonRecursive, Recursive };
    explicit QMutex(RecursionMode mode = NonRecursive);
    };

    ————————————————–
    class QMutexData
    {
    public:
    bool recursive;
    QMutexData(QMutex::RecursionMode mode = QMutex::NonRecursive)
    : recursive(mode == QMutex::Recursive) {}
    };

    ————————————————–
    class QMutexPrivate : public QMutexData {
    public:
    QMutexPrivate();
    bool wait(int timeout = -1);
    void wakeUp();
    // Conrol the lifetime of the privates
    QAtomicInt refCount;
    int id;

    bool ref() {
    Q_ASSERT(refCount.load() >= 0);
    int c;
    do {
    c = refCount.load();
    if (c == 0)
    return false;
    } while (!refCount.testAndSetRelaxed(c, c + 1));
    Q_ASSERT(refCount.load() >= 0);
    return true;
    }
    void deref() {
    Q_ASSERT(refCount.load() >= 0);
    if (!refCount.deref())
    release();
    Q_ASSERT(refCount.load() >= 0);
    }
    void release();
    static QMutexPrivate *allocate();
    QAtomicInt waiters; //number of thread waiting
    QAtomicInt possiblyUnlocked; //bool saying that a timed wait timed out
    enum { BigNumber = 0×100000 }; //Must be bigger than the possible number of waiters (number of threads)
    void derefWaiters(int value);
    bool wakeup;
    pthread_mutex_t mutex;
    pthread_cond_t cond;
    };
    ———————————-
    QMutex的类层次上面已经展现了,我们来看看怎么构造一个QMutex对象吧:

    QMutex::QMutex(RecursionMode mode)
    {
    d_ptr.store(mode == Recursive ? new QRecursiveMutexPrivate : 0);
    }
    其中的d_ptr是在QBasicMutex中定义的:

    QBasicAtomicPointer<QMutexData> d_ptr;
    根据QMutex构造时的参数,将QMutexData中的recursive成员赋值:默认是0,也就是QMutex::NonRecursive。

    b、使用lock(),那么lock()又是怎么实现的呢?

    从上面的类型层次可以看出,这个接口是QBasicMutex类实现的,如下:

    inline void lock() {
    if (!fastTryLock())
    lockInternal();
    }

    也就是说,必须是fastTryLock()返回值为0才有实际的动作,那fastTryLock()又是什么呢?

    inline bool fastTryLock() {
    return d_ptr.testAndSetAcquire(0, dummyLocked());
    }

    testAndSetAcquire()又是什么呢?
    ****************************************************************************************
    原型:bool testAndSetAcquire(T *expectedValue, T *newValue);

    对于x86平台来说,实现在arch\qatomic_i386.h中:

    template <typename T>
    Q_INLINE_TEMPLATE bool QBasicAtomicPointer<T>::testAndSetAcquire(T *expectedValue, T *newValue)
    {
    return testAndSetOrdered(expectedValue, newValue);
    }

    testAndSetOrdered(expectedValue, newValue)又是怎样实现的?根平台和编译器相关,对于gemfield本文来说,就是 Linux上的GCC编译器,那么:

    template <typename T>
    Q_INLINE_TEMPLATE bool QBasicAtomicPointer<T>::testAndSetOrdered(T *expectedValue, T *newValue)
    {
    unsigned char ret;
    asm volatile(“lock\n”
    “cmpxchgl %3,%2\n”
    “sete %1\n”
    : “=a” (newValue), “=qm” (ret), “+m” (_q_value)
    : “r” (newValue), “0″ (expectedValue)
    : “memory”);
    return ret != 0;
    }

    **************************************************************************************
    d_ptr.testAndSetAcquire(0, dummyLocked());的含义就是判断d_ptr当前的值是不是0,如果是0的话,则将dummyLocked()的值赋给d_ptr,并返回真值;否则什么都不做,并返回false。

    static inline QMutexData *dummyLocked() {
    return reinterpret_cast<QMutexData *>(quintptr(1));
    }

    对不起了,各位,我刚洗了个澡回来。我发现照这样写下去本文就写不完了。我决定把本文介绍的内容的底层实现部分放在《Qt的原子操作》一文之后,本文从简介绍下互斥、读写锁、条件变量、信号量这些概念及用法。所以,上面红颜色装饰的内容就先不要看了。

    第二部分、QMutexLocker的诞生

    QMutexLocker相当于QMutex的简化,提供了简化了的互斥上的操作(也即简化了的加锁和解锁)。

    QMutex实现的互斥功能用的不是挺好的吗?怎么又出现了一个QMutexLocker?其实不然,观察下面的这个代码:
    ****************************************************************
    int complexFunction(int flag)
    {
    mutex.lock();
    int retVal = 0;

    switch (flag) {
    case 0:
    case 1:
    mutex.unlock();
    return moreComplexFunction(flag);
    case 2:
    {
    int status = anotherFunction();
    if (status < 0) {
    mutex.unlock();
    return -2;
    }
    retVal = status + flag;
    }
    break;
    default:
    if (flag > 10) {
    mutex.unlock();
    return -1;
    }
    break;
    }

    mutex.unlock();
    return retVal;
    }
    *******************************************************************
    上面的代码真实的揭露了QMutex的无力,因为只要有mutex.lock(),必然要有mutex.unlock(),否则临界区里的资源将再不能被访问;而上面的代码并不能保证QMutex的对象一定会unlock(代码可能从某个地方就走了,再不回来了)。这个时候QMutexLocker就发挥用处了,因为QMutexLocker一定是以函数内部的局部变量的形式出现的,当它的作用域结束的时候,这个互斥就自然unlock了。代码如下:
    *******************************************************************
    int complexFunction(int flag)
    {
    QMutexLocker locker(&mutex);//定义的时候就上锁了

    int retVal = 0;

    switch (flag) {
    case 0:
    case 1:
    return moreComplexFunction(flag);
    case 2:
    {
    int status = anotherFunction();
    if (status < 0)
    return -2;
    retVal = status + flag;
    }
    break;
    default:
    if (flag > 10)
    return -1;
    break;
    }

    return retVal;//超出函数的作用域就解锁了
    }
    ******************************************************************

    第三部分:QReadWriteLock的作用

    虽然互斥的功能保证了临界区资源的安全,但是在某些方面并不符合实际;比如一般情况下,资源可以被并发读!举个实际的例子:有一本书(比如CivilNet BOOK),当某个人读到一页时,另外一个人(或者多个人)也可以过来读;但是,当1个人往上面写笔记时,其他人不能一起写,而且只有这个人把笔记写完了,再让大家一起看。

    QReadWriteLock的作用就是保证各个线程能并发的读某个资源,但是要写的话,就得真的lock了(所以,QReadWriteLock适合大量并发读,偶尔会有写的情况);代码如下:

    *************************************************************
    QReadWriteLock lock;

    void ReaderThread::run()

    {

    lock.lockForRead();
    read_file();
    lock.unlock();

    }

    void WriterThread::run()
    {

    lock.lockForWrite();
    write_file();
    lock.unlock();

    }
    **************************************************************
    特别的,对于lock这个全局锁来说:

    1、只要有任意一个线程lock.lockForWrite()之后,所有之后的lock.lockForRead()都将会被阻塞;
    2、只要有任意一个线程的lock.lockForWrite()动作还在被阻塞着的话,所有之后的lock.lockForRead()都会失败;
    3、如果在被阻塞的队列中既有lock.lockForWrite()又有lock.lockForRead(),那么write的优先级比read高,下一个执行的将会是lock.lockForWrite()。

    大多数情况下,QReadWriteLock都是QMutex的直接竞争者.和QMutex类似,QReadWriteLock也提供了它的简化类来应付复杂的加锁解锁(也是通过函数作用域的手段),代码如下:
    ****************************************************************
    QReadWriteLock lock;

    QByteArray readData()
    {
    QReadLocker locker(&lock);

    return data;
    }

    void writeData(const QByteArray &data)
    {
    QWriteLocker locker(&lock);

    }
    *************************************************************

    第四部分:QSemaphore 提供了QMutex的通用情况

    反过来,QMutex是QSemaphore的特殊情况,QMutex只能被lock一次,而QSemaphore却可以获得多次;当然了,那是因为Semaphores要保护的资源和mutex保护的不是一类;Semaphores保护的一 般是一堆相同的资源; 比如:
    1、mutex保护的像是洗手间这样的,只能供1人使用的资源(不是公共场所的洗手间);
    2、Semaphores保护的是像停车场、餐馆这样有很多位子资源的场所;

    Semaphores 使用两个基本操作acquire() 和 release():

    比如对于一个停车场来说,一般会在停车场的入口用个LED指示牌来指示已使用车位、可用车位等;你要泊车进去,那就要acquire(1)了,这样available()就会减一;如果你开车离开停车场 ,那么就要release(1)了,同时available()就会加一。

    让gemfield用代码来演示一个环形缓冲区和其上的信号量(生产-消费模型):

    const int DataSize = 1000;//这个店一共要向这个环形桌上供应1000份涮肉
    const int BufferSize = 100;//环形自助餐桌上可以最多容纳下100份涮肉
    char buffer[BufferSize];//buffer就是这个环形自助餐桌了

    QSemaphore freePlace(BufferSize);//freeBytes信号量控制的是没有放涮肉盘子的区域,初始化值是100,很明显,程序刚开始的时候桌子还是空的;
    QSemaphore usedPlace;//usedPlace 控制的是已经被使用的位置,也很明显,程序刚开始的时候,还没开始吃呢。

    好了,对于饭店配羊肉的服务员来说,
    class Producer : public QThread

    public:
    void run();//重新实施run虚函数
    };

    void Producer::run()
    {
    for (int i = 0; i < DataSize; ++i) {
    freePlace.acquire();//空白位置减一
    buffer[i % BufferSize] = “M”;//放肉(麦当劳 :-) )
    usedPlace.release();//已使用的位置加一
    }
    }

    服务员(producer)要生产1000份涮肉( DataSize), 当他要把生产好的一份涮肉往环形桌子上放之前,必须使用freePlace信号量从环形桌上获得一个空地方(一共就100个)。 如果消费 者吃的节奏没跟的上的话,QSemaphore::acquire() 调用可能会被阻塞。

    最后,服务员使用usedPlace信号量来释放一个名额。一个“空的位置”被成功的转变为“已被占用的位置”,而这个位置消费者正准备吃。

    对于食客(消费者)来说:

    class Consumer : public QThread
    {
    public:
    void run();//重新实施run虚函数
    };

    void Consumer::run()
    {
    //消费者一共要吃1000份涮肉
    for (int i = 0; i < DataSize; ++i) {
    usedPlace.acquire();//如果还没有位置被使用(表明没有放肉),阻塞
    eat(buffer[i % BufferSize]);
    freePlace.release();//吃完后,空白位置可以加一了
    }
    leaveCanting();
    }

    在main函数中,gemfield创建了2个线程,并且通过QThread::wait()来确保在程序退出之前,线程都已经执行完了(也即完成了各自的1000次for循环)

    int main(int argc, char *argv[])
    {
    QCoreApplication app(argc, argv);
    Producer producer;
    Consumer consumer;
    producer.start();
    consumer.start();
    producer.wait();
    consumer.wait();
    return 0;
    }

    程序是怎么运行的呢?

    初始的时候,只有服务员线程可以做任何事; 消费者线程被阻塞了——等待着usedPlace信号量被释放(available()初始值是0);当服务员把第一份涮肉放到桌子上的时候,
    freePlace.available() 的值就变为了BufferSize – 1, 并且usedPlace.available() is 1.这时,两个线程都可以工作了: 消费者可以吃这第一份涮肉,并且服务员再生产第二份涮肉;

    在一个多处理器的机器上,这个程序将有可能达到基于mutex的程序的2倍快, 因为两个线程可以同时工作在不同的缓冲区上。

    第五部分: QWaitCondition,与QSemaphore的竞争

    const int DataSize = 1000;//这个店一共要向这个环形桌上供应1000份涮肉
    const int BufferSize = 100;//环形自助餐桌上可以最多容纳下100份涮肉
    char buffer[BufferSize];

    QWaitCondition placeNotEmpty;//当有肉放上来,就发出这个信号
    QWaitCondition placeNotFull;//当消费者吃完一份涮肉后发出这个信号
    QMutex mutex;
    int numUsedPlace = 0;//已经放了肉的位置数
    为了同步服务员和消费者, 我们需要2个条件变量和1个mutex。变量解释参考上面的代码注释。让我们看看服务员这个生产者的类:
    ************************************************
    class Producer : public QThread
    {
    public:
    void run();
    };
    void Producer::run()
    {
    for (int i = 0; i < DataSize; ++i) {
    mutex.lock();
    if (numUsedBytes == BufferSize)
    placeNotFull.wait(&mutex);
    mutex.unlock();

    buffer[i % BufferSize] = “M”;(又是麦当劳)

    mutex.lock();
    ++numUsedBytes;
    placeNotEmpty.wakeAll();
    mutex.unlock();
    }
    }
    ******************************************************

    在服务员将肉放到环形桌上之前,先要检查下桌子是不是放满了。如果满了,服务员就等待placeNotFull条件.

    在最后,服务员将numUsedBytes自增1,并且发出bufferNotEmpty条件是真的这个信号,因为numUsedBytes肯定大于0;

    注意,QWaitCondition::wait() 函数使用一个mutex作为它的参数,这样做的意义是:mutex刚开始是lock的,然后当这个线程因为placeNotFull.wait(&mutex);而休眠时,这个mutex就会被unlock,而当这个线程被唤醒时,mutex再次被加锁。

    另外, 从locked状态到wait状态是原子的,以此来防止竞态条件的发生。

    再来看看消费者类:
    **************************************************
    class Consumer : public QThread
    {
    public:
    void run();
    };

    void Consumer::run()
    {
    for (int i = 0; i < DataSize; ++i) {
    mutex.lock();
    if (numUsedBytes == 0)
    placeNotEmpty.wait(&mutex);
    mutex.unlock();

    eat(buffer[i % BufferSize]);

    mutex.lock();
    –numUsedBytes;
    placeNotFull.wakeAll();
    mutex.unlock();
    }
    leaveCanting();
    }
    ***************************************************
    代码和服务员的差不多。再来看看main函数:
    ***************************************************
    int main(int argc, char *argv[])
    {
    QCoreApplication app(argc, argv);
    Producer producer;
    Consumer consumer;
    producer.start();
    consumer.start();
    producer.wait();
    consumer.wait();
    return 0;
    }
    ***************************************************
    和信号一节差不多,程序刚开始的时候,只有服务员线程可以做一些事;消费者被阻塞(等肉),直到“位置不为空”信号发出。

    在一个多处理器的机器上,这个程序将有可能达到基于mutex的程序的2倍快, 因为两个线程可以同时工作在不同的缓冲区上。

    其实,Qt的线程库所包含的内容正是gemfield上一文《从pthread到QThread》中介绍的QThread类,以及QMutexLocker, QReadWriteLock, QSemaphore, QWaitCondition这些类,再外加一个atomic原子操作的内容(这时gemfield下一篇文章的内容哦);了解了这些,我们就可以更加自信的使用Qt的线程库了

    展开全文
  • 事件的互不相容性和独立性,在学习的时候突然觉得这两者之间总是存在是若即若离的关系,一时间不知道怎么去归纳总结好。一直找了很多信息来看,那么究竟应该怎么去区别或者什么时候使用比较好呢? 问题 互不相容:...

    前言

    事件的互不相容性和独立性,在学习的时候突然觉得这两者之间总是存在是若即若离的关系,一时间不知道怎么去归纳总结好。一直找了很多信息来看,那么究竟应该怎么去区别或者什么时候使用比较好呢?

    问题

    互不相容:事件A与事件B不可能同时发生。即在同一样本空间内,A与B没有交集,P(AUB)=P(A+B)=P(A)+P(B)-P(AB),因为P(AB)=0,所以P(A+B)=P(A)+P(B)

    独立:事件A发生的可能性,不影响事件B发生的可能性。根据条件概率可知有P(AB)=P(A)P(B|A),因为A不影响B,所以有P(AB)=P(A)P(B),那么也可以在同一样本空间里有P(AUB)=P(A+B)=P(A)+P(B)-P(AB)=P(A+B)=P(A)+P(B)-P(A)P(B)

    所以,我的疑惑有,互不相容实在同一样本空间,是不是独立其实也可以在同一样本空间内,只要A事件不影响B事件的发生即可。

    分析

    举例分析:

    1. 以掷骰子为例,掷1次骰子,事件A 得到点数为2,事件B 得到点数6,那么A与B,有你没我。即互不相容
    2. 以掷骰子为例,掷2次骰子,事件A 第一次得到点数为2,事件B第二次 得到点数6,那么A发生不影响B也发生。即独立

    在这里问题来了,我就在想例1和例2,掷骰子次数不一样,例2相当于重复了例1两次,感觉这样对比没什么意义。那么在例1的样本空间里面,会不会有独立事件,可以同时解释互不相容和独立。

    1. 以掷骰子为例,掷1次骰子,事件的内容定义应该会影响到两者的关系。如事件A得到点数2,事件B得到点数为6。那么A发生概率为1/6,B发生概率1/6,但是A发生,B就不可能发生,两者互不相容。
    2. 接上,事件A得到点数为2,事件B得到点数为10,A的发生概率为1/6,但是B为不可能事件,永远也不会发生,那么A就不影响B,两者即独立。同样的B事件可定义为数字在1-6之间,成为必然事件,两者也独立。当然这是极端情况。
    3. 接上。假设A得到点数为2,B的到点数为偶数,两者是A包含于B的关系。A发生时,B就发生了;A不发生,B也有可能发生。所以既不是互不相容,也不是独立。P(AB)=P(A)。
    4. 接上。假设A得到点数2,B是骰子落在桌子右侧。事件A与事件B明显互不影响。因为A与B是同一次试验中两个不同维度的事件问题,在各自的维度上有各自的概率模型。那么两者可称独立事件。如同直角坐标系的X与Y两者范围相交。有P(A∩B)/P(B)=P(A)/Ω。
    5. 最后,翻阅陈希孺院士著作《概率论与数理统计》,其对于独立事件的判断说,公式P(B)=P(A)P(B)概率的乘法定理,虽未定义,但不常用于判断是否为独立事件。判断两个事件是否独立,仍然要从事件的实际角度去分析判断其不应有关联,才可是独立,可应用概率的乘法公式计算。我思考了一下,觉得有道理,与其纠结,不如在实际问题中思考,公式的含义一般都来源与现实的时间,不然P(AB)=P(A)P(B|A) 到 P(AB)=P(A)P(B)的转化是如何而来的呢,就是因为人为判断了A事件不影响B事件的发生。现实的主观性判断带来了独立事件的概念和定义。

    结论

    到最后也恍然大悟,在探索的过程中,确实有收获,希望能帮助一些不太理解的朋友。本人也是一知半解,希望读者不吝赐教。

    展开全文
  • 四种进程或线程同步互斥的控制方法1、临界区:通过对多线程的串行化来访问公共资源或一段... 4、事 件:用来通知线程有一些事件已发生,从而启动后继任务的开始。 一 临界区临界区的使用在线程同步中应该算是比较简单

    四种进程或线程同步互斥的控制方法
    1、临界区:通过对多线程的串行化来访问公共资源或一段代码,速度快,适合控制数据访问。 
    2、互斥量:为协调共同对一个共享资源的单独访问而设计的。 
    3、信号量:为控制一个具有有限数量用户资源而设计。 
    4、事 件:用来通知线程有一些事件已发生,从而启动后继任务的开始。

     

    一 临界区

    临界区的使用在线程同步中应该算是比较简单,说它简单还是说它同后面讲到的其它方法相比更容易理 解。举个简单的例子:比如说有一个全局变量(公共资源)两个线程都会对它进行写操作和读操作,如 果我们在这里不加以控制,会产生意想不到的结果。假设线程A正在把全局变量加1然后打印在屏幕上, 但是这时切换到线程B,线程B又把全局变量加1然后又切换到线程A,这时候线程A打印的结果就不是程 序想要的结果,也就产生了错误。解决的办法就是设置一个区域,让线程A在操纵全局变量的时候进行 加锁,线程B如果想操纵这个全局变量就要等待线程A释放这个锁,这个也就是临界区的概念。

     

    二 互斥体
    windows api中提供了一个互斥体,功能上要比临界区强大。也许你要问,这个东东和临界区有什么区别
    ,为什么强大?它们有以下几点不一致:
    1.critical section是局部对象,而mutex是核心对象。因此像waitforsingleobject是不可以等待临界区的。
    2.critical section是快速高效的,而mutex同其相比要慢很多
    3.critical section使用范围是单一进程中的各个线程,而mutex由于可以有一个名字,因此它是可以应用
    于不同的进程,当然也可以应用于同一个进程中的不同线程。
    4.critical section 无法检测到是否被某一个线程释放,而mutex在某一个线程结束之后会产生一
    个abandoned的信息。同时mutex只能被拥有它的线程释放。 下面举两个应用mutex的例子,一个是程序只能运行一个实例,也就是说同一个程序如果已经运行了, 就不能再运行了;另一个是关于非常经典的哲学家吃饭问题的例子。

     

    三 事件
    事件对象的特点是它可以应用在重叠I/O(overlapped I/0)上,比如说socket编程中有两种模型,一种是
    重叠I/0,一种是完成端口都是可以使用事件同步。它也是核心对象,因此可以被waitforsingleobje这些 函数等待;事件可以有名字,因此可以被其他进程开启。

     

    四 信号量
    semaphore的概念理解起来可能要比mutex还难, 我先简单说一下创建信号量的函数,因为我在开始使
    用的时候没有很快弄清楚,可能现在还有理解不对的地方,如果有错误还是请大侠多多指教。
    CreateSemaphore(
      LPSECURITY_ATTRIBUTES lpSemaphoreAttributes,  // SD
      LONG lInitialCount,                                               // initial count
      LONG lMaximumCount,                                            // maximum count
      LPCTSTR lpName                                                   // object name
    )
    第一个参数是安全性,可以使用默认的安全性选项NULL;第二个和第三个参数是两个long型的数值,
    它们表示什么含义呢?lMaxinumCount表示信号量的最大值,必须要大于零。比如是5就表示可以有5个进程 或者线程使用,如果第六个进程或者线程想使用的话就必须进入等待队列等待有进程或者线程释放资源 。lInitalCount表示信号量的初始值,应该大于或者等于零小于等于lMaximumCount。如果lInitialCount  = 0 && lMaximumCount == 5,那么就表示当前资源已经全部被使用,如果再有进程或者线程想使用的 话,信号量就会变成-1,该进程或者线程进入等待队列,直到有进程或者线程执行ReleaseMutex;如 果lInitialCount = 5 && lMaximumCount == 5,那么就表示现在信号量可以被进程或者线程使用5次,再 之后就要进行等待;如果InitialCount = 2 && MaximumCount == 5这样的用法不太常见,表示还可 以调用两次CreateSemaphore或者OpenSemaphore,再调用的话就要进入等待状态。最后一个参数表 示这个信号量的名字,这样就可以跨进程的时候通过这个名字OpenSemaphore。

     

    总结: 
    1. 互斥量与临界区的作用非常相似,但互斥量是可以命名的,也就是说它可以跨越进程使用。所以创建互斥量需要的资源更多,所以如果只为了在进程内部是用的话使 用临界区会带来速度上的优势并能够减少资源占用量。因为互斥量是跨进程的互斥量一旦被创建,就可以通过名字打开它。 
    2. 互斥量(Mutex),信号灯(Semaphore),事件(Event)都可以被跨越进程使用来进行同步数据操作,而其他的对象与数据同步操作无关,但 对于进程和线程来讲,如果进程和线程在运行状态则为无信号状态,在退出后为有信号状态。所以可以使用WaitForSingleObject来等待进程和 线程退出。 
    3. 通过互斥量可以指定资源被独占的方式使用,但如果有下面一种情况通过互斥量就无法处理,比如现在一位用户购买了一份三个并发访问许可的数据库系统,可以根 据用户购买的访问许可数量来决定有多少个线程/进程能同时进行数据库操作,这时候如果利用互斥量就没有办法完成这个要求,信号灯对象可以说是一种资源计数 器。

     

     

    六。 CreateMutex()与 CreateEvent() 区别

    使用CreateMutex()来产生一个Mutex物件,而传入的Mutex名称字串用以区别不同的Mutex
    ,也就是说,不管是哪个Process/Thread,只要传入的名称叁数是相同的一个字串,那
    CreateMutex()传回值(hMutex, handle of Mutex)会指向相同的一个Mutex物件。这和
    Event物件相同。然而Mutex和Event有很大的不同,Mutex有Owner的概念,如果Mutex为
    ThreadA所拥有,那麽ThreadA执行WaitForSingleObject()时,并不会停下来,而会立即
    传回WAIT_OBJECT_0,而其他的Thread执行WaitForSingleObject()则会停下来,直到Mutex
    的所有权被Release出来或Time Out。

     

    如果一个Thread已取得Mutex的所有权,而它呼叫WaitForSingleObject()
    n 次,则也要使用ReleaseMutex n次才能够将Mutex的拥有权放弃,这和Event也不同,而
    且,非Mutex拥有者呼叫ReleaseMutex也不会有任何作用。而每次以WaitForSingleObject
    呼叫一次,Mutex会有一个计数器会加一,ReleaseMutex成功会减一,直到Mutex的计数
    器为0之後,系统才会将之去除。

    展开全文
  • 事件,临界区和互斥量,信号量都可以解决单生产者,消费者,缓冲区 多生产者,消费者,缓冲区,信号量解决最好 下面是事件解决单生产者,消费者,缓冲区 //1生产者 1消费者 1缓冲区 //使用二个事件,...
  • 同步和互斥

    2018-05-23 15:50:48
    线程同步(互斥锁与信号量的作用与区别)“信号量用在多线程多任务同步的,一个线程完成了某一个动作就通过信号量告诉别的线程,别的线程再进行某些动作(大家都在semtake的时候,就阻塞在 哪里)。而互斥锁是用在多...
  • 线程同步是指多线程通过特定的设置(如互斥量,事件对象,临界区)来控制线程之间的执行顺序(即所谓的同步)也可以说是在线程之间通过同步建立起执行顺序的关系,如果没有同步,那线程之间是各自运行各自的!...
  • 进程互斥与同步

    2019-04-23 16:10:00
    并行是指同时发生的两个并发事件,具有并发的含义,而并发则不一定并行,也亦是说并发事件之间不一定要同一时刻发生。 两者区别:一个是交替执行,一个是同时执行. 2、进程之间存在同步和互斥两种关系 同步是因合作...
  • 进程同步与互斥

    2021-09-20 11:39:54
    在多道程序系统中,由于资源共享或进程合作,使进程间形成间接相互制约和直接相互制约关系,这需要用进程互斥与同步机制来协调两种制约关系。 (进程互斥:指在多道程序环境下,每次只允许一个进程对临界资源进行访问...
  • 互斥与同步

    2014-08-23 23:24:28
    在多核对称处理器或者是可抢占型内核中,由于进程的...3互斥体 4CRU 一并发的来源 1多任务抢占型的内核,由“宏观并行,微观并行”带来的并发。 2中断。 3多任务处理器。同一时刻有多个处理器同时
  • Ucos2之互斥

    2019-08-31 13:44:35
    互斥锁是一种uCos2对资源保护的措施,比如任务需要访问某些资源的,但是这些资源不能同时被几个任务访问,这个时候就需要互斥锁这类的技术。互斥锁保护的资源要求,同时访问该资源的任务只能有一个,如果其他任务...
  • 这里写目录标题概览临界区临界区的引入临界区的概念进程的同步与互斥的概念解决方法经典同步与互斥问题 概览 临界区 临界区的引入 在系统当中,有些资源允许多个进程共享(磁盘),有些资源只允许进程单独使用...
  • Windows下线程同步互斥

    2017-09-25 00:26:22
    Windows下经典线程同步互斥
  • 在实际业务中往往会遇到多个线程对同一个数据操作,我们该如何处理呢?...正常情况下不过就是算数而已(剩余:1200元)。如果不做线程互斥,放在多线程并发...答案是如果不做线程互斥,就会出现极端情况(小概率事件...
  • 进程的同步与互斥

    2019-04-22 19:56:13
    系统中的某些资源一次只允许一个进程使用的资源临界资源称之为临界资源或者互斥资源共享资源。各进程采取互斥的方式,实现共享的资源称作临界资源。属于临界资源的硬件有,打印机,磁带机等;软件有消息队列,变量,...
  • 进出互斥与同步

    2019-04-26 13:44:00
     并行性指两个或两个以上事件或活动在同一时刻发生。在多道程序环境下,并行性使多个程序同一时刻可在不同CPU上同时执行。  并发与并行是两个既相似而又不相同的概念:并发性,又称共行性,是指能处理多个同时性...
  • 进程间互斥、同步

    2017-10-29 21:24:09
    进程间互斥、同步 并发、并行的区别 并发:是在同一个cpu上同时(不是真正的同时,而是看来是同时,因为cpu要在多个程序间切换)运行多个程序。 并行:是指两个或两个以上事件或活动在同一时刻发生。在多道...
  • 一、互斥 为何需要引入互斥机制? 当多个线程对同一数据并发读写(至少有一个线程执行写操作)时,这种情形被称为竞争。竞争会导致数据读或写的不确定性。而有时这种不确定性是不可容忍的。 如何实现互斥? 在...
  • 原文:https://blog.csdn.net/qq_32646795/article/details/78221005本文打算写一些和锁有关的东西,谈一谈我对锁的原理和实现的理解,主要包含以下方面信号量互斥量条件变量同步与互斥其实同步与互斥都是计算机科学...
  • 九、同步与互斥

    2018-04-22 21:37:11
    同步与互斥背景到目前为止多道程序设计(multi-programming):现代OS的重要特征并行很有用:(为什么?),提供了多个并行的实体:CPUs,I/O,...,用户进程/线程:OS抽象出来的用于支持多道程序设计CPU调度:实现多道...
  • UCOSIII中的互斥信号量

    2019-09-14 10:50:15
    文章目录序言优先级反转优先级反转实验互斥信号量互斥信号量实验任务内嵌信号量任务内嵌信号量实验 序言 我的上一篇文章介绍了信号量的基础知识并利用这些基础知识进行了一个小实验以此来增进对信号量这个概念的感性...
  • 互斥与同步的概念 在多道程序环境下,系统中可能有许多并发的进程,在这些进程之间存在以下两种关系:间接相互制约关系、直接相互制约关系。 间接相互制约关系 多个进程彼此无关,它们并不知道其他进程的存在。由...
  • 【UCOSIII】UCOSIII的事件标志组

    万次阅读 2018-07-05 19:10:09
    前面讲述了UCOSIII的信号量、互斥信号量,它们都可以完成任务的同步。但是有时候一个任务可能需要和多个事件同步,这个时候就需要使用事件标志组。事件标志组与任务之间有两种同步机制: “或”同步:等待多个事件...

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 14,912
精华内容 5,964
关键字:

互斥事件的含义