1.1 线程同步概述
如果没有同步对象和操作系统对特殊事件监视的能力,线程可能被迫使用有副作用的技术使自己与特殊事件同步。不使用操作系统支持的线程同步技术,会产生许多问题,比如:分配不必要的CPU时间,浪费;在高低优先级线程间,若低线程负责信号重置任务,则可能永远无法执行重置。
1.2 临界区
1.2.1 概述
临界区:在所有同步对象中,临界区是最容易使用的,但它只能用于同步单个进程中的线程。临界区一次只允许一个线程取得对某个数据区的访问权。还有,在这些同步对象中,只有临界区不是内核对象,它不由操作系统的低级部件管理,而且不能使用句柄来操纵。
1. 在进程中创建一个临界区,即在进程中分配一个CRITICAL_SECTION数据结构,该临界区结构的分配必须是全局的,这样该进程的不同线程就能访问它。
2. 在使用临界区同步线程之前,必须调用InitializeCriticalSection来初始化临界区。在释放资源之前,只需要初始化一次。
3. VOID EnterCriticalSection:阻塞函数。The function returns when the calling thread is granted ownership。换言之,调用线程不能获取指定临界区的所有权时,该线程将睡眠,且在被唤醒之前,系统不会给它分配CPU。
4. 执行临界区内的任务
5. BOOL LeaveCriticalSection:非阻塞函数。将当前线程对指定临界区的引用计数减壹;在使用计数变为零时,另一等待此临界区的一个线程将被唤醒。
6. 当 不需要再使用该临界区时,使用DeleteCriticalSection来释放临界区需要的资源。此函数执行后,再也不能使用 EnterCriticalSection和LeaveCriticalSection,除非再次使用 InitializeCriticalSection初始化了该临界区。
1.2.2 注意事项:
1. 临 界区一次只允许一个线程访问,每个线程必须在视图操作临界区域数据之前调用该临界区域标志(即一个CRITICAL_SECTION全局变量) EnterCriticalSection后,其它想要获得访问权的线程都会置于睡眠状态,且在被唤醒以前,系统将停止为它们分配CPU时间片。换言之,临界区可以且仅可被一个线程拥有,当然,没有任何线程调用EnterCriticalSection或TryEnterCriticalSection时,临界区不属于任何一个线程。
2. 当拥有临界区所有权的线程调用LeaveCriticalSection放弃所有权时,系统只唤醒正等待中的一个线程,给它所有权,其它线程则继续睡眠。
3. 注意,拥有该临界区的线程,每一次针对此临界区的EnterCriticalSection调用都会成功(这里指的是重复调用也会立即返回),且会使得临界区标志(即一个CRITICAL_SECTION全局变量)的引用计数增壹。 在另一个线程能够拥有该临界区之前,拥有它的线程必须调用LeaveCriticalSection足够多次,在引用计数降为零后,另一线程才有可能拥有 该临界区。换言之,在一个正常使用临界区的线程中,calSection和LeaveCriticalSection应该成对使用。
4. TryEnterCriticalSection
BOOL TryEnterCriticalSection(
LPCRITICAL_SECTION lpCriticalSection从函数声明便可看出端倪,EnterCriticalSection函数的返回值为VOID,而这里为BOOL。 可见对于TryEnterCriticalSection的调用,需要我们判断其返回值。在调用TryEnterCriticalSection时,如果 指定的临界区没有被任何线程(或还没有被任何调用线程)拥有,该函数将临界区的访问权给予调用的线程,并返回TRUE;不过,如果临界区已经被另一个线程 拥有,它立刻返回FALSE值。TryEnterCriticalSection和EnterCriticalSection之间的最大区别在于TryEnterCriticalSection从来不挂起线程。
);
1.3 用内核对象同步线程
1.3.1 概述
临界区非常适合于序列化对一个进程中的数据的访问,因为它们的速度很快。但我们或许想要使一些应用程序与计算机中发生的其它特殊事件或者其它进程中执行的操作取得同步。这时临界区无能为力。就需要使用内核对象来同步。
下列内核对象可用来同步线程:
1. 进程,Processes
2. 线程,Threads
3. 文件,Files
4. 控制台输入,Console input
5. 文件变化通知,File change notifications
6. 互斥量,Mutexes
7. 信号量,Semaphores
8. 事件(自动重设事件和手动重设事件),Events
9. 可等的计时器(只用于Window NT4或更高),Waitable timers
10. Jobs
每一个上面这些类型的对象都可以处于两种状态之一:有信号(signaled)和无信号(nonsignaled)。比如进程和线程在终结时其内核对象变为有信号,而在它们处于创建和正在运行时,其内核对象是无信号的。
1.3.2 内核对象同步应用
1. 某线程获得某进程的内核对象句柄时,它可以:改变进程优先级、获得进程的退出码;使本线程与某进程的终结取得同步等等。
2. 当获得某线程的内核对象句柄时,它可以:改变该线程运行状态、与该线程的终结取得同步等等。
3. 当获得文件句柄时,也可以:本线程可与某一个异步文件的I/O操作获得同步等等。
4. 控制台输入对象可用来使线程在有输入进入时被唤醒以执行相关任务等等。
5. 其它内核对象???文件改变通知、互斥量、信号量、事件、可等计时器等???都只是为了同步对象而存在。相应的,也有WIN32函数来创建、打开、关闭这些对象,将线程与这些对象同步。对这些对象,没有其它操作可以执行了。
1.3.3 互斥量独有的特性(另参附录的实验)
互斥量对象与所有其它内核对象的不同之处在于它是被线程所拥有的。其 它所有同步对象要么有信号,要么无信号,仅此而已。而互斥量对象除了记录当前信号状态外,还要记住此时那个线程拥有它。如果一个线程在得到一个互斥量对象 (即将其置为无信号态)后就终结了,互斥量也就废弃了。在这种情况了,互斥量将永远保持无信号态,因为没有其它线程能够通过调用ReleaseMutex 来释放它。
系统发现产生这种情况时,就自动将互斥量设回有信号状态。其它等待该信号量的线程就会被唤醒,但函数的返回值为WAIT_ABANDONED而不是正常的WAIT_OBJECT_0。这时,其它线程可以知道互斥量是不是被正常释放。
其它的,互斥量与CRITICAL_SECTION类似。拥有该互斥量的线程,每次调用WaitForSingleObject都会立即成功返回,但互斥量的使用计数将增加,同样的,也要多次调用ReleaseMutex以使引用计数变为零,方可供别的线程使用。
1.3.3.1 疑问
问:其它内核对象在线程异常终止没有释放所有权时,系统回重置其状态吗?如果重置,将没有任何标记,与正常释放无异,即不会拥有互斥量的这个返回WAIT_ABANDONED的特性?
【注意:线程拥有某个内核对象和线程拥有某个内核对象的所有权,这二者是不同的。当说线程拥有某个内核对象时,要强调的是当该线程终止时,若线程正好拥有该内核对象的访问权,内核对象也将被废弃???因为不能重置其信号状态;而线程拥有某一个内核对象的所用权,指的是线程可以调用某些函数,访问该内核对象或对该内核对象执行某些操作】
答:见附录中后面的“讨论与实验一”。
1.3.4 WaitForSingleObject与WaitForMultipleObjects
线程主要使用两个函数将它们设为睡眠来等待内核对象变为有信号:即都是阻塞函数。
DWORD WaitForSingleObject(
HANDLE hHandle,
DWORD dwMilliseconds
);
DWORD WaitForMultipleObjects(
DWORD nCount,
const HANDLE* lpHandles,
BOOL bWaitAll,
DWORD dwMilliseconds
);
WaitForSingleObject, 在一个指定时间(dwMilliseconds)内等待某一个内核对象变为有信号,在此时间内,若等待的内核对象一直是无信号的,则调用线程将睡眠,否则 继续执行。超过此时间后,线程继续运行。函数返回值可能为:WAIT_OBJECT_0、WAIT_TIMEOUT、WAIT_ABANDONED(仅当 内核对象为互斥量时)、WAIT_FAILED。
WaitForMultipleObjects与 WaitForSingleObject类似,只是它要么等待指定列表(由lpHandles指定)中若干个对象(由nCount决定)都变为有信号,要 么等待一个列表(由lpHandles指定)中的某一个对象变为有信号(由bWaitAll决定)。
WaitForSingleObject 和WaitForMultipleObjects函数对特定的内核对象有重要的副作用???即它们根据不同的内核对象,会决定是否改变内核对象的信号状 态,并执行这种改变;这些副作用,决定了是让等待该内核对象的进程或线程中的某一个被唤醒还是全都被唤醒。
1、 对进程和线程内核对象,这两个函数不产生副作用。即,在进程或线程内核对象变为有信号后,它们将保持有信号,这两个函数不会试图改变内核对象的信号状态。这样,所有等待这些内核对象的线程都会被唤醒。
The WaitForSingleObject function checks the current state of the specified object. If the object~s state is nonsignaled, the calling thread enters the wait state. It uses no processor time while waiting for the object state to become signaled or the time-out interval to elapse.
The function modifies the state of some types of synchronization objects. Modification occurs only for the object whose signaled state caused the function to return. For example, the count of a semaphore object is decreased by one.
2、 对于互斥量、自动重置事件和自动重置可等的计时器对象,这两个函数将把它们的状态改为无信号。换言之,一旦这些对象变为有信号并且有一个线程被唤醒,则对象重被置为无信号状态。于是,只有一个正在等待的线程醒来,其它等待的线程将继续睡眠。
3、 对于WaitForMultipleObjects函数还有非常重要的一个特性:当调用它时传递的bWaitAll为TRUE时,在所有被等待的对象都变为有信号之前,被等待的任何可以被改变状态的内核对象都不被重置为无信号状态。换言之,在传入参数bWaitAll为TRUE,WaitForMultipleObjects除非能取得所有指定对象(由lpHandles指定)的所有权,它不会取得单个对象的所有权(不能取得所有权,自然也不会改变此对象的信号状态)。这是为了防止死锁。换言之,在bWaitAll为TRUE时,WaitForMultipleObjects不会在没有获得所有被等对象所有权的情形下改变某一可以被改变状态的内核对象的信号状态,任何以同样方式等待的线程都不会被唤醒???但以其它方式等待的线程将被唤醒。
The WaitForMultipleObjects function determines whether the wait criteria have been met. If the criteria have not been met, the calling thread enters the wait state. It uses no processor time while waiting for the criteria to be met.
When bWaitAll is TRUE, the function~s wait operation is completed only when the states of all objects have been set to signaled. The function does not modify the states of the specified objects until the states of all objects have been set to signaled. For example, a mutex can be signaled, but the thread does not get ownership until the states of the other objects are also set to signaled. In the meantime, some other thread may get ownership of the mutex, thereby setting its state to nonsignaled.
The function modifies the state of some types of synchronization objects. Modification occurs only for the object or objects whose signaled state caused the function to return. For example, the count of a semaphore object is decreased by one. When bWaitAll is FALSE, and multiple objects are in the signaled state, the function chooses one of the objects to satisfy the wait(到底选择哪一个呢?天知道!); the states of the objects not selected are unaffected.
1.3.5 事件或定时器的自动重置与手动重置
1. 自动重置:在内核对象变为有信号时,仅有一个线程可以获得它,该线程一旦获得,内核对象又将变为无信号态,其它等待的线程将继续睡眠。由Wait类函数实现自动重置未无信号状态的功能。
2. 手动重置:一般来说,所有等待该信号的线程都会苏醒过来。在你调用相关函数将内核对象置为无信号状态前,内核对象将一直处于信号状态。
2 附录
2.1 讨论与实验一:内核对象处于非信号状态时,相关线程结束时内核对象是否被系统重置为信号台?
互 斥量被某一线程拥有,若线程退出但未释放相应互斥量,操作系统将调用ReleaseMutex释放相应互斥量。对于信号量和事件呢?若没有被正确释放或重 置,操作系统是否在进程结束后尝试像对互斥量进行释放的操作???若不,那么,对没有正确释放的信号量和事件,在操作系统的本次运行期间将一直存在,是 么?:举例来说,若进程使用某一事件来完成唯一实例,若进程退出后没有正确释放相应事件,那么,在系统重新启动之前,进程将不能运行。是么?
注 意,对于内核对象来说,若其引用计数变为0,系统将销毁该内核对象。为了比较,我以Event和Mutex分别构造几个应用程序。(注意,Mutex是属 于线程的,但这里的应用程序都是单线程的,换言之,这里没有测试到进程内某一拥有互斥量的线程不释放互斥量便退出的情况!)
2.1.1 Event程序
使 用Event的应用程序有四。Event创建时置为手动重置事件,初始状态为有信号状态,换言之,以::CreateEvent(NULL, TRUE, TRUE, "KernelObjectsEvent ")方式创建。还有,以WaitForSingleObject(内核对象句柄,0)方式等待“KernelObjectsEvent”名的Event对 象。
1. Event_OnlyReferences~仅仅尝试用CreateEvent获取Event句柄,不等待该Event,也不改变它的任何状态。
2. Event_AfterWaitnonSignaled~同样方式调用CreateEvent,然后等待,成功后置为无信号态。应用程序继续运行。
3. Event_setEventBeforeWaitAfternonSignaled~同样方式调用CreateEvent,但在等待前置事件为信号台,然后等待,成功后置为无信号态。
4. Event_setEventBeforeWaitAfterSignaled~同样方式调用CreateEvent,但在等待前置事件为信号台,然后等待,成功后置为有信号态。
2.1.2 Mutex程序
使 用Mutex的应用程序有四。Mutex创建时置为非进程拥有,换言之,以CreateMutex(NULL, FALSE, "KernelObjectsMutex")方式创建。还有,以WaitForSingleObject(内核对象句柄,0)方式等待 “KernelObjectsMutex”名的Mutex对象
1. Mutex_OnlyReferences~仅仅尝试用CreateMutex获取Mutex句柄,不等待该Mutex,也不会尝试释放它。
2. Mutex_AfternonSignaled~同样方式调用CreateMutex,然后等待,成功后并不尝试释放它。应用程序继续运行。
3. Mutex_ReleaseBeforeWaitAfternonSignaled~同样方式调用CreateMutex,但在等待前尝试多次调用ReleaseMutex释放它,然后等待,成功后并不尝试释放它。
4. Mutex_ReleaseBeforeWaitAfterSignaled~同样方式调用CreateMutex,但在等待前尝试多次调用ReleaseMutex释放它,然后等待,成功后置再次释放它。