进程/线程间同步
2006-03-29 10:47
281 查看
进程/线程间同步 document.title="进程/线程间同步 - "+document.title
这一节的内容比较多请你耐心的看完,因为进程/线程间同步的方法比较多,每种方法都有不同的用途:这节中会讲通过临界区,互斥量,信号灯,事件来进行同步。
由于进程/线程间的操作是并行进行的,所以就产生了一个数据的问题同步,我们先看一段代码:
现在假设有两个线程threadA1和threadA2在同时运行那么运行结束后iCounter的值会是多少,是200吗?不是的,如果我们将Sleep(1000)前的注释去掉后我们会很容易明白这个问题,因为在iCounter的值被正确修改前它可能已经被其他的线程修改了。这个例子是一个将机器代码操作放大的例子,因为在CPU内部也会经历数据读/写的过程,而在线程执行的过程中线程可能被中断而让其他线程执行。变量iCounter在被第一个线程修改后,写回内存前如果它又被第二个线程读取,然后才被第一个线程写回,那么第二个线程读取的其实是错误的数据,这种情况就称为脏读(dirty read)。这个例子同样可以推广到对文件,资源的使用上。
那么要如何才能避免这一问题呢,假设我们在使用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 );退出临界区,相当于申请解锁
下面的示范代码演示了如何使用临界区来进行数据同步处理:
接下来要讲互斥量与临界区的作用非常相似,但互斥量是可以命名的,也就是说它可以跨越进程使用。所以创建互斥量需要的资源更多,所以如果只为了在进程内部是用的话使用临界区会带来速度上的优势并能够减少资源占用量。因为互斥量是跨进程的互斥量一旦被创建,就可以通过名字打开它。下面介绍可以用在互斥量上的API函数:
你会说为什么没有名称如同EnterMutex,功能如同EnterCriticalSection一样的函数来获得互斥量的使用权呢?的确没有!获取互斥量的使用权需要使用函数:
在线程调用WaitForSingleObject后,如果一直无法得到控制权线程讲被挂起,直到超过时间或是获得控制权。
讲到这里我们必须更深入的讲一下WaitForSingleObject函数中的对象(Object)的含义,这里的对象是一个具有信号状态的对象,对象有两种状态:有信号/无信号。而等待的含义就在于等待对象变为有信号的状态,对于互斥量来讲如果正在被使用则为无信号状态,被释放后变为有信号状态。当等待成功后WaitForSingleObject函数会将互斥量置为无信号状态,这样其他的线程就不能获得使用权而需要继续等待。WaitForSingleObject函数还进行排队功能,保证先提出等待请求的线程先获得对象的使用权,下面的代码演示了如何使用互斥量来进行同步,代码的功能还是进行全局变量递增,通过输出结果可以看出,先提出请求的线程先获得了控制权:
在这里我没有使用全局变量来保存互斥量句柄,这并不是因为不能这样做,而是为演示如何在其他的代码段中通过名字来打开已经创建的互斥量。其实这个例子在逻辑上是有一点错误的,因为iCounter这个变量没有跨进程使用,所以没有必要使用互斥量,只需要使用临界区就可以了。假设有一组进程在同时使用一个文件那么我们可以使用互斥量来保证该文件只同时被一个进程使用(如果只是利用OS的文件存取控制功能则需要添加更多的错误处理代码),此外在调度程序中也可以使用互斥量来对资源的使用进行同步化。
现在我们回过头来讲WaitForSingleObject这个函数,从前面的例子中我们看到WaitForSingleObject这个函数将等待一个对象变为有信号状态,那么具有信号状态的对象有哪些呢?下面是一部分:
Mutex
Event
Semaphore
Job
Process
Thread
Waitable timer
Console input
互斥量(Mutex),信号灯(Semaphore),事件(Event)都可以被跨越进程使用来进行同步数据操作,而其他的对象与数据同步操作无关,但对于进程和线程来讲,如果进程和线程在运行状态则为无信号状态,在退出后为有信号状态。所以我们可以使用WaitForSingleObject来等待进程和线程退出。(至于信号灯,事件的用法我们接下来会讲)我们在前面的例子中使用了WaitForMultipleObjects函数,这个函数的作用与WaitForSingleObject类似但从名字上我们可以看出,WaitForMultipleObjects将用于等待多个对象变为有信号状态,函数原型如下:
返回值意义:
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:表示超过规定时间。
通过互斥量我们可以指定资源被独占的方式使用,但如果有下面一种情况通过互斥量就无法处理,比如现在一位用户购买了一份三个并发访问许可的数据库系统,你的老板会要求你根据用户购买的访问许可数量来决定有多少个线程/进程能同时进行数据库操作,这时候如果利用互斥量就没有办法完成这个要求,信号灯对象可以说是一种资源计数器。对信号灯的操作伪代码大致如下:
这里信号灯有一个初始值,表示有多少进程/线程可以进入,当信号灯的值大于0时为有信号状态,小于等于0时为无信号状态,所以可以利用WaitForSingleObject进行等待,当WaitForSingleObject等待成功后信号灯的值会被减少1,直到释放时信号灯会被增加1。用于信号灯操作的API函数有下面这些:
可以看出来信号灯的使用方式和互斥量的使用方式非常相似,下面的代码使用初始值为2的信号灯来保证只有两个线程可以同时进行数据库调用:
信号灯有时用来作为计数器使用,一般来讲将其初始值设置为0,先调用ReleaseSemaphore来增加其计数,然后使用WaitForSingleObject来减小其计数,遗憾的是通常我们都不能得到信号灯的当前值,但是可以通过设置WaitForSingleObject的等待时间为0来检查信号灯当前是否为0。
接下来我们讲最后一种同步对象:事件,前面讲的信号灯和互斥量可以保证资源被正常的分配和使用,而事件是用来通知其他进程/线程某件操作已经完成。例如:现在有三个线程:threadA,threadB,threadC,现在要求他们中的部分功能要顺序执行,也就是说threadA执行完一部分后threadB执行,threadB执行完一部分后threadC开始执行。也许你觉得下面的代码可以满足要求:
可以看出来方案三的执行时间是最短的,当然这个例子有些极端,但我们可以看出事件对象用于通知其他进程/线程某件操作已经完成方面的作用是很大的,而且如果有的任务要在进程尖进行协调采用等待其他进程中线程结束的方式是不可能实现的。此外我也希望通过这个例子讲一点关于分析线程执行效率的方法。
事件对象可以一两种方式创建,一种为自动重置,在其他线程使用WaitForSingleObject等待到事件对象变为有信号后该事件对象自动又变为无信号状态,一种为人工重置在其他线程使用WaitForSingleObject等待到事件对象变为有信号后该事件对象状态不变。例如有多个线程都在等待一个线程运行结束,我们就可以使用人工重置事件,在被等待的线程结束时设置该事件为有信号状态,这样其他的多个线程对该事件的等待都会成功(因为该事件的状态不会被自动重置)。事件相关的API如下:
下面的代码演示了自动重置和人工重置事件在使用中的不同效果:
从执行结果中我们可以看到在第二次执行时由于使用了自动重置事件threadB中只有一个线程能够等待到threadA中释放的事件对象。
在处理多进程/线程的同步问题时必须要小心避免发生死锁问题,比如说现在有两个互斥量A和B,两个线程tA和tB,他们在执行前都需要得到这两个互斥量,但现在这种情况发生了,tA拥有了互斥量A,tB拥有了互斥量B,但它们同时都在等待拥有另一个互斥量,这时候显然谁也不可能得到自己希望的资源。这种互相拥有对方所拥有的资源而且都在等待对方拥有的资源的情况就称为死锁。关于这个问题更详细的介绍请参考其他参考书。
在MFC中对于各种同步对象都提供了相对应的类
在这些类中封装了上面介绍的对象创建,打开,控制,删除功能。但是如果要使用等待功能则需要使用另外两个类:CSingleLock和CMultiLock。这两个类中封装了WaitForSingleObject和WaitForMultipleObjects函数。如果大家觉的需要可以看看这些类的定义,我想通过上面的介绍可以很容易理解,但是在对象同步问题上我觉得使用API函数比使用MFC类更为直观和方便。
下载本节示范代码 25K
这一节的内容比较多请你耐心的看完,因为进程/线程间同步的方法比较多,每种方法都有不同的用途:这节中会讲通过临界区,互斥量,信号灯,事件来进行同步。
由于进程/线程间的操作是并行进行的,所以就产生了一个数据的问题同步,我们先看一段代码:
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
相关文章推荐
- 进程-线程-同步-互斥
- 【进程线程与同步】5.3 创建与联接线程
- 自定义的互斥量类,可以用于线程或进程的同步
- 线程进程同步(占坑)
- 【安卓开发艺术探索】第2章 进程线程通信与同步 笔记
- 阻塞 非阻塞 同步 异步 线程 进程 任务
- 进程、线程知识点总结和同步(消费者生产者,读者写者三类问题)、互斥、异步、并发、并行、死锁、活锁的总结
- 操作系统课程设计-线程和进程的同步与互斥
- 线程,进程间的通讯和同步原理,实现用例和应用
- 进程、线程通信与同步
- C#与NET实战 第5章 进程、线程与同步 节选
- 多线程, 进程, 线程间同步
- WinCE 进程、线程和内存管理之同步
- Python 中的进程、线程、协程、同步、异步、回调
- GCD(一) ---- 进程、线程、队列、同步、异步 概念区分与使用
- 【进程线程与同步】5.3 创建与联接线程
- linux进程和线程之间通信方法和同步方法总结
- ReaderWriterLock(定义支持单个写线程和多个读线程的锁),Mutex(一个同步基元,也可用于进程间同步。 )
- 线程进程同步(占坑)
- Linux 进程与线程的同步与互斥