临界区
在所有同步对象中,临界区是最容易使用的,但它不是内核对象,只能用于同步单个进程中的线程。临界区一次只允许一个线程取得对某个数据区的访问权。还有,在这些同步对象中,只有临界区不是内核对象,它不由操作系统的低级部件管理,而且不能使用句柄来操纵。
由于使用时不需要从用户态切换到核心态,所以速度很快(X86系统上约为20个指令周期),非常适合于序列化对一个进程中的数据的访问,但其缺点是不能跨进程同步,同时不能指定阻塞时的等待时间,只能无限等待。
注意下面几点:
1 临界区一次只允许一个线程访问,每个线程必须在操作临界区域数据之前要调用EnterCriticalSection,获取访问权后,其它想要获得访问权的线程都会置于睡眠状态,且在被唤醒以前,系统将停止为它们分配CPU时间片。换言之,临界区可以且仅可被一个线程拥有,当然,没有任何线程调用EnterCriticalSection或TryEnterCriticalSection时,临界区不属于任何一个线程。
2 当拥有临界区所有权的线程调用LeaveCriticalSection放弃所有权时,系统只唤醒正等待中的一个线程,给它所有权,其它线程则继续睡眠。
3 注意,拥有该临界区的线程,每一次针对此临界区的EnterCriticalSection调用都会成功(这里指的是重复调用也会立即返回),且会使得临界区(即一个CRITICAL_SECTION全局变量)的引用计数增壹。在另一个线程能够拥有该临界区之前,拥有它的线程必须调用LeaveCriticalSection足够多次,在引用计数降为零后,另一线程才有可能拥有该临界区。换言之,在一个正常使用临界区的线程中,EnterCriticalSection和LeaveCriticalSection应该成对使用。
4 TryEnterCriticalSection,如果指定的临界区没有被任何线程拥有,该函数将临界区的访问权给予调用的线程,并返回TRUE;不过,如果临界区已经被另一个线程拥有,它立刻返回FALSE值。
互斥量
与临界区功能类似,也可以用来做线程间的同步,但区别在于:互斥量是内核对象,可以实现跨进程互斥,但需要在用户态和核心态之间切换,速度比临界区慢得多(X86系统上约为600个指令周期),同时可以指定阻塞时的等待时间。还可以用来保证程序只有一个实例运行(创建命名互斥量,再次创建该命名的互斥量时,CreateMutex函数返回已创建互斥量的句柄,但GetLastError返回ERROR_ALREADY_EXISTS)
互斥量独特之处在于它是被线程所拥有的,所以除了记住它的状态之外,还要记住此时那个线程拥有它。如果一个线程在得到一个互斥量对象(即将其置为无信号态)后就终结了,互斥量也就废弃了。在这种情况了,互斥量将永远保持无信号态,因为没有其它线程能够通过调用ReleaseMutex来释放它。
系统发现产生这种情况时,就自动将互斥量设回有信号状态。其它等待该信号量的线程就会被唤醒,但函数的返回值为WAIT_ABANDONED而不是正常的WAIT_OBJECT_0。这时,其它线程可以知道互斥量是不是被正常释放。拥有该互斥量的线程,每次调用WaitForSingleObject都会立即成功返回,但互斥量的使用计数将增加,也要多次调用ReleaseMutex以使引用计数变为零,方可供别的线程使用。
总结一下,互斥量与临界区有下面几点不同:
1. 临界区是局部对象,而mutex是核心对象。因此像waitforsingleobject是不可以等待临界区的。
2. 临界区是快速高效的,而mutex同其相比要慢很多;
3. 临界区使用范围是单一进程中的各个线程,而mutex由于可以有一个名字,因此它是可以应用于不同的进程,当然也可以应用于同一个进程中的不同线程。
4. 临界区无法检测到是否被某一个线程释放,如果拥有它的线程不释放它,即使该线已经终止,其他线程也无法访问该临界区。mutex也是只能被拥有它的线程释放,但如果线程在结束之前没有调用ReleaseMutex释放它,系统会将其设成信号态,并产生WAIT_ABANDONED信息。
事件
事件也是内核对象,具有“信号态”和“无信号态”两种状态。当某一线程等待一个事件时,如果事件为信号态,将继续执行,如果事件为无信号态,那么线程被阻塞。线程能够指定阻塞时的等待时间。多用于线程间的通信,通过OpenEvent打开一个命名的事件对象,也可以用来跨进程同步。
有两种工作模式:手动重置和自动重置。
1. 自动重置:在内核对象变为有信号时,仅有一个线程可以获得它,该线程一旦获得,系统会自动调用ResetEvent将该内核对象变为无信号态,其它等待的线程将继续睡眠。但如果内核对象处于信号态后,一直没有线程获得它,它将保持这种状态,并不会被自动重置。
2. 手动重置:除非显式调用相关函数将内核对象置为无信号态,内核对象将一直处于信号态。一般来说,所有等待该信号的线程都会苏醒过来。
信号量
信号量是一个资源计数器,当某线程获取某信号量时,信号量计数首先减1,如果计数小于0,那么该线程被阻塞;当某县城释放某信号量时,信号量计数首先加1,如果计数小于或等与0,那么唤醒某被阻塞的线程并执行之。对信号量的总结如下:
1 如果计数器m大于0,表示还有m个资源可以访问,此时信号量线程等待队列中没有线程被阻塞,新的线程访问资源也不会被阻塞;
2 如果计数器m等与0,表示没有资源可以访问,此时信号量线程等待队列中没有线程被阻塞,但新的线程访问资源会被阻塞;
3 如果计数器m小于0,表示没有资源可以访问,此时信号量线程等待队列中有abs(m)个线程被阻塞,新的线程访问资源会被阻塞;
信号量常被用于保证对多个资源进行同步访问。譬如到银行办业务、或者到车站买票, 原来只有一个服务员, 不管有多少人排队等候, 业务只能一个个地来. 假如增加了业务窗口, 可以同时受理几个业务呢? 这就类似与 Semaphore 对象, Semaphore 可以同时处理等待函数(如: WaitForSingleObject)申请的几个线程.
Semaphore的工作思路如下:
1、首先要通过CreateSemaphore(安全设置, 初始信号数, 信号总数, 信号名称) 建立信号对象;
参数四: 和 Mutex 一样, 它可以有个名称, 也可以没有, 本例就没有要名称(nil); 有名称的一般用于跨进程.
参数三: 信号总数, 是 Semaphore 最大处理能力, 就像银行一共有多少个业务窗口一样;
参数二: 初始信号数, 这就像银行的业务窗口很多, 但打开了几个可不一定, 如果没打开和没有一样;
参数一: 安全设置和前面一样, 使用默认(nil)即可.
2、要接受 Semaphore服务(或叫协调)的线程, 同样需要用等待函数(如: WaitForSingleObject)排队等候;
3、当一个线程使用完一个信号,应该用ReleaseSemaphore(信号句柄, 1, nil) 让出可用信号给其他线程;
参数三: 一般是 nil, 如果给个数字指针, 可以接受到此时(之前)总共闲置多少个信号;
参数二: 一般是 1, 表示增加一个可用信号;
如果要增加CreateSemaphore 时的初始信号, 也可以通过 ReleaseSemaphore.
4、最后, 作为系统内核对象, 要用 CloseHandle 关闭.
另外, 在 Semaphore 的总数是 1 的情况下, 就和 Mutex(互斥) 一样了.
生产者-消费者问题是一个经典的进程同步问题,该问题最早由Dijkstra提出, 用以演示他提出的信号量机制。在同一个进程地址空间内执行的两个线程。生产者线程生产物品,然后将物品放置在一个空缓冲区中供消费者线程消费。消费者线程从缓冲区中获得物品,然后释放缓冲区。当生产者线程生产物品时,如果没有空缓冲区可用,那么生产者线程必须等待消费者线程释放出一个空缓冲区。当消费者线程消费物品时,如果没有满的缓冲区,那么消费者线程将被阻塞,直到新的物品被生产出来。