线程的基础知识

 1. 进程与线程有那些区别和联系?
     每个进程至少需要一个线程。
         进程由两部分构成:进程内核对象,地址空间。线程也由两部分组成:线程内核对象,操作系统用它来对线程实施管理。线程堆栈,用于维护线程在执行代码时需要的所有函数参数和局部变量。
        进程是不活泼的。进程从来不执行任何东西,它只是线程的容器。线程总是在某个进程环境中创建的,而且它的整个寿命期都在该进程中。
        如果在单进程环境中,有多个线程正在运行,那么这些线程将共享单个地址空间。这些线程能够执行相同的代码,对相同的数据进行操作。这些线程还能共享内核对象句柄,因为句柄表依赖于每个进程而不是每个线程存在。
        进程使用的系统资源比线程多得多。实际上,线程只有一个内核对象和一个堆栈,保留的记录很少,因此需要很少的内存。因此始终都应该设法用增加线程来解决编程问题,避免创建新的进程。但是许多程序设计用多个进程来实现会更好些。

2. 如何使用_beginthreadex函数?
         使用方法与CreateThread函数相同,只是调用参数类型需要转换。

3. 如何使用CreateThread函数?
         当CreateThread被调用时,系统创建一个线程内核对象。该线程内核对象不是线程本身,而是操作系统用来管理线程的较小的数据结构。使用时应当注意在不需要对线程内核进行访问后调用CloseHandle函数关闭线程句柄。因为CreateThread函数中使用某些C/C++运行期库函数时会有内存泄漏,所以应当尽量避免使用。
参数含义:
lpThreadAttributes  如果传递NULL该线程使用默认安全属性。如果希望所有的子进程能够继承该线程对象的句柄,必须将它的bInheritHandle成员被初始化为TRUE。
dwStackSize  设定线程堆栈的地址空间。如果非0,函数将所有的存储器保留并分配给线程的堆栈。如果是0,CreateThread就保留一个区域,并且将链接程序嵌入.exe文件的/STACK链接程序开关信息指明的存储器容量分配给线程堆栈。
lpStartAddress  线程函数的地址。
lpParameter  传递给线程函数的参数。
dwCreationFlags  如果是0,线程创建后立即进行调度。如果是CREATE_SUSPENDED,系统对它进行初始化后暂停该线程的运行。
LpThreadId  用来存放系统分配给新线程的ID。

4. 如何终止线程的运行?
(1)   线程函数返回(最好使用这种方法)。
这是确保所有线程资源被正确地清除的唯一办法。
如果线程能够返回,就可以确保下列事项的实现:
在线程函数中创建的所有C++对象均将通过它们的撤消函数正确地撤消。
操作系统将正确地释放线程堆栈使用的内存。
系统将线程的退出代码设置为线程函数的返回值。
系统将递减线程内核对象的使用计数。
(2)   调用ExitThread函数(最好不要使用这种方法)。
该函数将终止线程的运行,并导致操作系统清除该线程使用的所有操作系统资源。但是,C++资源(如C++类对象)将不被撤消。
(3)    调用TerminateThread函数(应该避免使用这种方法)。
TerminateThread能撤消任何线程。线程的内核对象的使用计数也被递减。TerminateThread函数是异步运行的函数。如果要确切地知道该线程已经终止运行,必须调用WaitForSingleObject或者类似的函数。当使用返回或调用ExitThread的方法撤消线程时,该线程的内存堆栈也被撤消。但是,如果使用TerminateThread,那么在拥有线程的进程终止运行之前,系统不撤消该线程的堆栈。
(4)    包含线程的进程终止运行(应该避免使用这种方法)。
由于整个进程已经被关闭,进程使用的所有资源肯定已被清除。就像从每个剩余的线程调用TerminateThread一样。这意味着正确的应用程序清除没有发生,即C++对象撤消函数没有被调用,数据没有转至磁盘等等。
一旦线程不再运行,系统中就没有别的线程能够处理该线程的句柄。然而别的线程可以调GetExitcodeThread来检查由hThread标识的线程是否已经终止运行。如果它已经终止运行,则确定它的退出代码。

5. 为什么不要使用_beginthread函数和_endthread函数?
 与_beginthreadex函数相比参数少,限制多。无法创建暂停的线程,无法取得线程ID。_endthread函数无参数,线程退出代码必须为0。还有_endthread函数内部关闭了线程的句柄,一旦退出将不能正确访问线程句柄。

6. 如何对进程或线程的内核进行引用?
HANDLE GetCurrentProcess(  );
HANDLE GetCurrentThread(  );
这两个函数都能返回调用线程的进程的伪句柄或线程内核对象的伪句柄。伪句柄只能在当前的进程或线程中使用,在其它线程或进程将不能访问。函数并不在创建进程的句柄表中创建新句柄。调用这些函数对进程或线程内核对象的使用计数没有任何影响。如果调用CloseHandle,将伪句柄作为参数来传递,那么CloseHandle就会忽略该函数的调用并返回FALSE。
DWORD GetCurrentProcessId(  );
DWORD GetCurrentThreadId(  );
这两个函数使得线程能够查询它的进程的唯一ID或它自己的唯一ID。

7. 如何将伪句柄转换为实句柄?
HANDLE hProcessFalse = NULL;
HANDLE hProcessTrue = NULL;
HANDLE hThreadFalse = NULL;
HANDLE hThreadTrue = NULL;

hProcessFalse = GetCurrentProcess(  );
hThreadFalse = GetCurrentThread(  );
取得线程实句柄:
DuplicateHandle( hProcessFalse, hThreadFalse, hProcessFalse, &hThreadTrue, 0, FALSE, DUPLICATE_SAME_ACCESS );
取得进程实句柄:
DuplicateHandle( hProcessFalse, hProcessFalse, hProcessFalse, &hProcessTrue, 0, FALSE, DUPLICATE_SAME_ACCESS );
由于DuplicateHandle会递增特定对象的使用计数,因此当完成对复制对象句柄的使用时,应该将目标句柄传递给CloseHandle,从而递减对象的使用计数。

8. 在一个进程中可创建线程的最大数是得多少?
线程的最大数取决于该系统的可用虚拟内存的大小。默认每个线程最多可拥有至多1MB大小的栈的空间。所以,至多可创建2028个线程。如果减少默认堆栈的大小,则可以创建更多的线程。

线程的调度、优先级和亲缘性
1. 如何暂停和恢复线程的运行?
        线程内核对象的内部有一个值指明线程的暂停计数。当调用CreateProcess或CreateThread函数时,就创建了线程的内核对象,并且它的暂停计数被初始化为1。因为线程的初始化需要时间,不能在系统做好充分的准备之前就开始执行线程。线程完全初始化好了之后,CreateProcess或CreateThread要查看是否已经传递了CREATE_SUSPENDED标志。如果已经传递了这个标志,那么这些函数就返回,同时新线程处于暂停状态。如果尚未传递该标志,那么该函数将线程的暂停计数递减为0。当线程的暂停计数是0的时候,除非线程正在等待其他某种事情的发生,否则该线程就处于可调度状态。在暂停状态中创建一个线程,就能够在线程有机会执行任何代码之前改变线程的运行环境(如优先级)。一旦改变了线程的环境,必须使线程成为可调度线程。方法如下:
hThread = CreatThread( ……,CREATE_SUSPENDED,…… );

bCreate = CreatProcess( ……,CREATE_SUSPENDED,……,pProcInfo );
if( bCreate != FALSE )
{
 hThread = pProcInfo.hThread;
}
……
……
……
ResumeThread( hThread );
CloseHandle( hThread );
ResumeThread成功,它将返回线程的前一个暂停计数,否则返回0xFFFFFFFF。
单个线程可以暂停若干次。如果一个线程暂停了3次,它必须恢复3次。创建线程时,除了使用CREATE_SUSPENDED外,也可以调用SuspendThread函数来暂停线程的运行。任何线程都可以调用该函数来暂停另一个线程的运行(只要拥有线程的句柄)。线程可以自行暂停运行,但是不能自行恢复运行。与ResumeThread一样,SuspendThread返回的是线程的前一个暂停计数。线程暂停的最多次数可以是MAXIMUM_SUSPEND_COUNT次。SuspendThread与内核方式的执行是异步进行的,但是在线程恢复运行之前,不会发生用户方式的执行。调用SuspendThread时必须小心,因为不知道暂停线程运行时它在进行什么操作。只有确切知道目标线程是什么(或者目标线程正在做什么),并且采取强有力的措施来避免因暂停线程的运行而带来的问题或死锁状态,SuspendThread才是安全的。

2. 是否可以暂停和恢复进程的运行?
         对于Windows来说,不存在暂停或恢复进程的概念,因为进程从来不会被安排获得CPU时间。不过Windows确实允许一个进程暂停另一个进程中的所有线程的运行,但是从事暂停操作的进程必须是个调试程序。特别是,进程必须调用WaitForDebugEvent和ContinueDebugEvent之类的函数。由于竞争的原因,Windows没有提供其他方法来暂停进程中所有线程的运行。

3. 如何使用sleep函数?
         系统将在大约的指定毫秒数内使线程不可调度。Windows不是个实时操作系统。虽然线程可能在规定的时间被唤醒,但是它能否做到,取决于系统中还有什么操作正在进行。
可以调用Sleep,并且为dwMilliseconds参数传递INFINITE。这将告诉系统永远不要调度该线程。这不是一件值得去做的事情。最好是让线程退出,并还原它的堆栈和内核对象。可以将0传递给Sleep。这将告诉系统,调用线程将释放剩余的时间片,并迫使系统调度另一个线程。但是,系统可以对刚刚调用Sleep的线程重新调度。如果不存在多个拥有相同优先级的可调度线程,就会出现这种情况。

4. 如何转换到另一个线程?
        系统提供了SwitchToThread函数。当调用这个函数的时候,系统要查看是否存在一个迫切需要CPU时间的线程。如果没有线程迫切需要CPU时间,SwitchToThread就会立即返回。如果存在一个迫切需要CPU时间的线程,SwitchToThread就对该线程进行调度(该线程的优先级可能低于调用SwitchToThread的线程)。这个迫切需要CPU时间的线程可以运行一个时间段,然后系统调度程序照常运行。该函数允许一个需要资源的线程强制另一个优先级较低、而目前却拥有该资源的线程放弃该资源。如果调用SwitchToThread函数时没有其他线程能够运行,那么该函数返回FALSE,否则返回一个非0值。调用SwitchToThread与调用Sleep是相似的。差别是SwitchToThread允许优先级较低的线程运行;而即使有低优先级线程迫切需要CPU时间,Sleep也能够立即对调用线程重新进行调度。

5. 如何取得线程运行的时间?
(1)   简单取得线程大概运行时间:
DWORD dwStartTime = 0;
DWORD dwEndTime = 0;
DWORD dwRunTime = 0;
dwStartTime = GetTickCount(  );
……
……
……
dwEndTime = GetTickCount(  );
dwRunTime = dwEndTime – dwStartTime;
(2)    调用GetThreadTimes的函数:
参数含义:
hThread 线程句柄
lpCreationTime 创建时间:英国格林威治时间
lpExitTime 退出时间:英国格林威治时间,如果线程仍然在运行,退出时间则未定义
lpKernelTime 内核时间:指明线程执行操作系统代码已经经过了多少个100ns的CPU时间
lpUserTime 用户时间:指明线程执行应用程序代码已经经过了多少个100ns的CPU时间
GetProcessTimes是个类似GetThreadTimes的函数,适用于进程中的所有线程(甚至是已经终止运行的线程)。返回的内核时间是所有进程的线程在内核代码中经过的全部时间的总和。GetThreadTimes和GetProcessTimes这两个函数在Windows98中不起作用。在Windows98中,没有一个可靠的机制可供应用程序来确定线程或进程已经使用了多少CPU时间。

6. 进程的优先级类有哪些?
优先级类 标识符 描述
实时    REALTIME_PRIORITY_CLASS 立即对事件作出响应,执行关键时间的任务。会抢先于操作系统组件之前运行。
高    HIGH_PRIORITY_CLASS 立即对事件作出响应,执行关键时间的任务。
高于正常    ABOVE_NORMAL_PRIORITY_CLASS 在正常优先级与高优先级之间运行(Windows2000)。
正常    NORMAL_PRIORITY_CLASS 没有特殊调度需求
低于正常    BELOW_NORMAL_PRIORITY_CLASS 在正常优先级与空闲优先级之间运行(Windows2000)。
空闲    IDLE_PRIORITY_CLASS 在系统空闲时运行。
设置方法:
BOOL SetPriorityClass( HANDLE hProcess, DWORD dwPriority );
DWORD GetPriorityClass( HANDLE hProcess );
使用命令外壳启动一个程序时,该程序的起始优先级是正常优先级。如果使用Start命令来启动该程序,可以使用一个开关来设定应用程序的起始优先级。例如:
c:/>START /LOW CALC.EXE
Start命令还能识别/BELOWNORMAL、/NORMAL、/ABOVENORMAL、/HIGH和/REALTIME等开关。

7. 线程的相对优先级有哪些?
相对优先级 标识符 描述
关键时间    THREAD_PRIORITY_TIME_CRITICAL 对于实时优先级类线程在优先级31上运行,对于其他优先级类,线程在优先级15上运行。
最高    THREAD_PRIORITY_HIGHEST 线程在高于正常优先级上两级上运行。
高于正常    THREAD_PRIORITY_ABOVE_NORMAL 线程在正常优先级上一级上运行。
正常    THREAD_PRIORITY_NORMAL 线程在进程的优先级类上正常运行。
低于正常    THREAD_PRIORITY_BELOW_NORMAL 线程在低于正常优先级下一级上运行。
最低    THREAD_PRIORITY_LOWEST 线程在低于正常优先级下两级上运行。
空闲    THREAD_PRIORITY_IDLE 对于实时优先级类线程在优先级16上运行对于其他优先级类线程在优先级1上运行。
设置方法:
BOOL SetThreadPriority( HANDLE hThread, DWORD dwPriority );
DWORD GetThreadPriorityClass( HANDLE hThread );

8. 如何避免系统动态提高线程的优先级等级?
        系统常常要提高线程的优先级等级,以便对窗口消息或读取磁盘等I/O事件作出响应。或者当系统发现一个线程在大约3至4s内一直渴望得到CPU时间,它就将这个渴望得到CPU时间的线程的优先级动态提高到15,并让该线程运行两倍于它的时间量。当到了两倍时间量的时候,该线程的优先级立即返回到它的基本优先级。下面的函数可以对系统的调度方式进行设置:
BOOL SetProcessPriorityBoost( HANDLE hProcess, BOOL bDisableBoost );
BOOL GetProcessPriorityBoost( HANDLE hProcess, PBOOL pbDisableBoost );
BOOL SetThreadPriorityBoost( HANDLE hThread, BOOL bDisableBoost );
BOOL GetThreadPriorityBoost( HANDLE hThread, PBOOL pbDisableBoost );
SetProcessPriorityBoost负责告诉系统激活或停用进行中的所有线程的优先级提高功能,而SetThreadPriorityBoost则激活或停用各个线程的优先级提高功能。Windows98没有提供这4个函数的有用的实现代码。

用户方式中线程的同步
1. 仅一条语句用不用考虑线程同步的问题?
当使用高级语言编程时,我们往往会认为一条语句是最小的原子访问,CPU不会在这条语句中间运行其他的线程。这是错误的,因为即使非常简单的一条高级语言的语句,经编译器编译后也可能变成多行代码由计算机来执行。因此必须考虑线程同步的问题。任何线程都不应该通过调用简单的C语句来修改共享的变量。

2. 互锁函数有那些?
(1) LONG InterlockedExchangeAdd ( LPLONG Addend, LONG Increment );
Addend为长整型变量的地址,Increment为想要在Addend指向的长整型变量上增加的数值(可以是负数)。这个函数的主要作用是保证这个加操作为一个原子访问。
(2) LONG InterlockedExchange( LPLONG Target, LONG Value );
用第二个参数的值取代第一个参数指向的值。函数返回值为原始值。
(3) PVOID InterlockedExchangePointer( PVOID *Target, PVOID Value );
用第二个参数的值取代第一个参数指向的值。函数返回值为原始值。
(4) LONG InterlockedCompareExchange(
LPLONG Destination, LONG Exchange, LONG Comperand  );
如果第三个参数与第一个参数指向的值相同,那么用第二个参数取代第一个参数指向的值。函数返回值为原始值。
(5) PVOID InterlockedCompareExchangePointer (
PVOID *Destination, PVOID Exchange, PVOID Comperand );
如果第三个参数与第一个参数指向的值相同,那么用第二个参数取代第一个参数指向的值。函数返回值为原始值。

3. 为什么单CPU的计算机不应该使用循环锁?
举例说明:
BOOL g_bResourceUse = FALSE;
……
void ThreadFunc1(  )
{
 BOOL bResourceUse = FALSE;
 while( 1 )
{
 bResourceUse = InterlockedExchange( &g_bResourceUse, TRUE );
 if( bResourceUse == FALSE )
 {
  break;
 }
 Sleep( 0 );
}
……
……
……
InterlockedExchange( &g_bResourceUse, FALSE );
}
首先循环锁会浪费CPU时间。CPU必须不断地比较两个值,直到一个值由于另一个线程而“奇妙地”改变为止。而且使用该循环锁的线程都应该为同一优先级,并且应当使用SetProcessPriorityBoost函数或SetThreadPriorityBoost函数禁止线程优先级的动态提高功能,否则优先级较低的线程可能永远不能被调用。

4. 如何使用volatile声明变量?
如果是对共享资源的地址进行使用如&g_Resource那么可以不使用volatile,因为将一个变量地址传递给一个函数时,该函数必须从内存读取该值。优化程序不会对它产生任何影响。如果直接使用变量,必须有一个volatile类型的限定词。它告诉编译器,变量可以被应用程序本身以外的某个东西进行修改,这些东西包括操作系统,硬件或同时执行的线程等。volatile限定词会告诉编译器,不要对该变量进行任何优化,并且总是重新加载来自该变量的内存单元的值。否则编译器会把变量的值存入CPU寄存器,每次对寄存器进行操作。线程就会进入一个无限循环,永远无法唤醒。

5. 如何使用关键代码段实现线程的同步?
如果需要一小段代码以原子操作的方式执行,这时简单的互锁函数已不能满足需要,必须使用关键代码段来解决问题。不过使用关键代码段时,很容易陷入死锁状态,因为在等待进入关键代码段时无法设定超时值。关键代码段是通过对共享资源设置一个标志来实现的,就像厕所门上的“有人/没人”标志一样。这个标志就是一个CRITICAL_SECTION变量。该变量在任何一个线程使用它之前应当进行初始化。初始化可以有两种方法,使用InitializeCriticalSection函数和InitializeCriticalSectionAndSpinCount函数。然后在每个使用共享资源的线程函数的关键代码段前使用EnterCriticalSection函数或者使用TryEnterCriticalSection函数。在关键代码段使用之后调用LeaveCriticalSection函数。在所有的线程都不再使用该共享资源后应当调用DeleteCriticalSection函数来清除该标志。举例说明:
const int MAX_TIMES = 1000;
int  g_intIndex = 0;
DWORD g_dwTimes[MAX_TIMES];
CRITICAL_SECTION g_cs;

void Init(  )
{
 ……
 InitializeCriticalSection( &g_cs );
……
}

DWORD WINAPI FirstThread( PVOID lpParam )
{
 while ( g_intIndex < MAX_TIMES )
{
 EnterCriticalSection( &g_cs );
 g_dwTimes[g_intIndex] = GetTickCount(  );
 g_intIndex++;
 LeaveCriticalSection( &g_cs );
}
return 0;
}

DWORD WINAPI SecondThread( PVOID lpParam )
{
 while ( g_intIndex < MAX_TIMES )
{
 EnterCriticalSection( &g_cs );
 g_intIndex++;
 g_dwTimes[g_intIndex - 1] = GetTickCount(  );
 LeaveCriticalSection( &g_cs );
}
return 0;
}

void Close(  )
{
……
DeleteCriticalSection( &g_cs );
……
}
使用关键代码段应当注意一些技巧:
(1) 每个共享资源使用一个CRITICAL_SECTION变量。
这样在当前线程占有一个资源时,另一个资源可以被其他线程占有。
EnterCriticalSection( &g_cs );
for ( intLoop = 0; intLoop < 100; intLoop++ )
{
g_intArray[intLoop] = 0;
g_uintArray[intLoop] = 0;
}
LeaveCriticalSection( &g_cs );
改为:
EnterCriticalSection( &g_csInt );
for ( intLoop = 0; intLoop < 100; intLoop++ )
{
g_intArray[intLoop] = 0;
}
LeaveCriticalSection( &g_csInt );
EnterCriticalSection( &g_csUint );
for ( intLoop = 0; intLoop < 100; intLoop++ )
{
g_uintArray[intLoop] = 0;
}
LeaveCriticalSection( &g_csUint );
(2) 同时访问多个资源,必须始终按照完全相同的顺序请求对资源的访问。
这样才能避免死锁状态产生。离开的顺序没有关系。
Thread1:
EnterCriticalSection( &g_csInt );
EnterCriticalSection( &g_csUint );
for ( intLoop = 0; intLoop < 100; intLoop++ )
{
g_uintArray[intLoop] = g_intArray[intLoop];
}
LeaveCriticalSection( &g_csInt );
LeaveCriticalSection( &g_csUint );
Thread2:
EnterCriticalSection( &g_csUint );
EnterCriticalSection( &g_csInt );
for ( intLoop = 0; intLoop < 100; intLoop++ )
{
g_uintArray[intLoop] = g_intArray[intLoop];
}
LeaveCriticalSection( &g_csInt );
LeaveCriticalSection( &g_csUint );
改为:
Thread1:
EnterCriticalSection( &g_csInt );
EnterCriticalSection( &g_csUint );
for ( intLoop = 0; intLoop < 100; intLoop++ )
{
g_uintArray[intLoop] = g_intArray[intLoop];
}
LeaveCriticalSection( &g_csInt );
LeaveCriticalSection( &g_csUint );
Thread2:
EnterCriticalSection( &g_csInt );
EnterCriticalSection( &g_csUint );
for ( intLoop = 0; intLoop < 100; intLoop++ )
{
g_uintArray[intLoop] = g_intArray[intLoop];
}
LeaveCriticalSection( &g_csInt );
LeaveCriticalSection( &g_csUint );
(3) 不要长时间运行关键代码段。
EnterCriticalSection( &g_cs );
SendMessage( hWnd, WM_SOMEMSG, &g_s, 0 );
LeaveCriticalSection( &g_cs );
改为:
EnterCriticalSection( &g_cs );
sTemp = g_s;
LeaveCriticalSection( &g_cs );
SendMessage( hWnd, WM_SOMEMSG, &sTemp, 0 );

6. InitializeCriticalSection/InitializeCriticalSectionAndSpinCount差别?
        InitializeCriticalSection函数的返回值为空并且不会创建事件内核对象,比较节省系统资源,但是一旦发生两个或多个线程争用关键代码段的情况,如果内存不足,关键代码段可能被争用,同时系统可能无法创建必要的事件内核对象。这时EnterCriticalSection函数将会产生一个EXCEPTION_INVALID_HANDLE异常。这个错误非常少见。如果想对这种情况有所准备,可以有两种选择。可以使用结构化异常处理方法来跟踪错误。当错误发生时,既可以不访问关键代码段保护的资源,也可以等待某些内存变成可用状态,然后再次调用EnterCriticalSection函数。
        另一种选择是使用InitializeCriticalSectionAndSpinCount,第二个参数dwSpinCount中,传递的是在使线程等待之前它试图获得资源时想要循环锁循环迭代的次数。这个值可以是0至0x00FFFFFF之间的任何数字。如果在单处理器计算机上运行时调用该函数,该参数被忽略,并且始终设置为0。使用InitializeCriticalSectionAndSpinCount函数创建关键代码段,确保设置了dwSpinCount参数的高信息位。当该函数发现高信息位已经设置时,它就创建该事件内核对象,并在初始化时将它与关键代码段关联起来。如果事件无法创建,该函数返回FALSE。可以更加妥善地处理代码中的这个事件。如果事件创建成功,EnterCriticalSection将始终都能运行,并且决不会产生异常情况(如果总是预先分配事件内核对象,就会浪费系统资源。只有当代码不能容许EnterCriticalSection运行失败,或者有把握会出现争用现象,或者预计进程将在内存非常短缺的环境中运行时,才能预先分配事件内核对象)。

7. TryEnterCriticalSection和EnterCriticalSection的差别是什么?
        如果EnterCriticalSection将一个线程置于等待状态,那么该线程在很长时间内就不能再次被调度。实际上,在编写得不好的应用程序中,该线程永远不会再次被赋予CPU时间。TryEnterCriticalSection函数决不允许调用线程进入等待状态。它的返回值能够指明调用线程是否能够获得对资源的访问权。TryEnterCriticalSection发现该资源已经被另一个线程访问,它就返回FALSE。在其他所有情况下,它均返回TRUE。运用这个函数,线程能够迅速查看它是否可以访问某个共享资源,如果不能访问,那么它可以继续执行某些其他操作,而不必进行等待。如果TryEnterCriticalSection函数确实返回了TRUE,那么CRITICAL_SECTION的成员变量已经更新。Windows98没有可以使用的TryEnterCriticalSection函数的实现代码。

 

 

Win32下两种用于C++的线程同步类(上)

2006-10-20 05:00 作者:Jerry Cat 出处:博客园 责任编辑:方舟
  线程同步是多线程程序设计的核心内容,它的目的是正确处理多线程并发时的各种问题,例如线程的等待、多个线程访问同一数据时的互斥,防死锁等。Win32提供多种内核对象和手段用于线程同步,如互斥量、信号量、事件、临界区等。所不同的是,互斥量、信号量、事件都是Windows的内核对象,当程序对这些对象进行控制时会自动转换到核心态,而临界区本身不是内核对象,它是工作在用户态的。我们知道从用户态转换到核心态是需要以时间为代价的,所以如果能在用户态就简单解决的问题,就可以不必劳烦核心态了。

  这里我要说的是两种用于C++的多线程同步类,通过对这两种类的使用就可以方便的实现对变量或代码段的加锁控制,从而防止多线程对变量不正确的操作。

  所谓加锁,就是说当我们要访问某关键变量之前,都需要首先获得允许才能继续,如果未获得允许则只有等待。一个关键变量拥有一把锁,一个线程必须先得到这把锁(其实称为钥匙可能更形象)才可以访问这个变量,而当某个变量持有这把锁的时候,其他线程就不能重复的得到它,只有等持有锁的线程把锁归还以后其他线程才有可能得到它。之所以这样做,就是为了防止一个线程读取某对象途中另一线程对它进行了修改,或两线程同时对一变量进行修改,例如:

// 全局:
struct MyStruct ... { int a, b; } ;
MyStruct s;
// 线程1:
int a = s.a;
int b = s.b;
// 线程2:
s.a ++ ;
s.b -- ;

  如果实际的执行顺序就是上述书写的顺序那到没有什么,但如果线程2的执行打断了线程1,变为如下顺序:

int a = s.a; //线程1
s.a++; //线程2
s.b++; //线程2
int b = s.b; //线程1

  那么这时线程1读出来的a和b就会有问题了,因为a是在修改前读的,而b是在修改后读的,这样读出来的是不完整的数据,会对程序带来不可预料的后果。天知道两个程的调度顺序是什么样的。为了防止这种情况的出现,需要对变量s加锁,也就是当线程1得到锁以后就可以放心的访问s,这时如果线程2要修改s,只有等线程1访问完成以后将锁释放才可以,从而保证了上述两线程交叉访问变量的情况不会出现。

  使用Win32提供的临界区可以方便的实现这种锁:

// 全局:
CRITICAL_SECTION cs;
InitializeCriticalSection( & cs);
// 线程1:
EnterCriticalSection( & cs);
int a = s.a;
int b = s.b;
LeaveCriticalSection( & cs);
// 线程2:
EnterCriticalSection( & cs);
s.a ++ ;
s.b -- ;
LeaveCriticalSection( & cs);
// 最后:
DeleteCriticalSection( & cs);

  代码中的临界区变量(cs)就可以看作是变量s的锁,当函数EnterCriticalSection返回时,当前线程就获得了这把锁,之后就是对变量的访问了。访问完成后,调用LeaveCriticalSection表示释放这把锁,允许其他线程继续使用它。

  如果每当需要对一个变量进行加锁时都需要做这些操作,显得有些麻烦,而且变量cs与s只有逻辑上的锁关系,在语法上没有什么联系,这对于锁的管理带来了不小的麻烦。程序员总是最懒的,可以想出各种偷懒的办法来解决问题,例如让被锁的变量与加锁的变量形成物理上的联系,使得锁变量成为被锁变量不可分割的一部分,这听起来是个好主意。

  首先想到的是把锁封闭在一个类里,让类的构造函数和析构函数来管理对锁的初始化和锁毁动作,我们称这个锁为“实例锁”:

class InstanceLockBase
... {
CRITICAL_SECTION cs;
protected :
InstanceLockBase() ... { InitialCriticalSection( & cs); }
~ InstanceLockBase() ... { DeleteCriticalSection( & cs); }
} ;

  如果熟悉C++,看到这里一定知道后面我要干什么了,对了,就是继承,因为我把构造函数和析构函数都声明为保护的(protected),这样唯一的作用就是在子类里使用它。让我们的被保护数据从这个类继承,那么它们不就不可分割了吗:

struct MyStruct: public InstanceLockBase
... { … } ;

  什么?结构体还能从类继承?当然,C++中结构体和类除了成员的默认访问控制不同外没有什么不一样,class能做的struct也能做。此外,也许你还会问,如果被锁的是个简单类型,不能继承怎么办,那么要么用一个类对这个简单类型进行封装(记得Java里有int和Integer吗),要么只好手工管理它们的联系了。如果被锁类已经有了基类呢?没关系,C++是允许多继承的,多一个基类也没什么。

  现在我们的数据里面已经包含一把锁了,之后就是要添加加锁和解锁的动作,把它们作为InstanceLockBase类的成员函数再合适不过了:

class InstanceLockBase
... {
 CRITICAL_SECTION cs;
 void Lock() ... { EnterCriticalSection( & cs); }
 void Unlock() ... { LeaveCriticalSection( & cs); }
 …
} ;

  看到这里可能会发现,我把Lock和Unlock函数都声明为私有了,那么如何访问这两个函数呢?是的,我们总是需要有一个地方来调用这两个函数以实现加锁和解锁的,而且它们总应该成对出现,但C++语法本身没能限制我们必须成对的调用两个函数,如果加完锁忘了解,那后果是严重的。这里有一个例外,就是C++对于构造函数和析构函数的调用是自动成对的,对了,那就把对Lock和Unlock的调用专门写在一个类的构造函数和析构函数中:

class InstanceLock
... {
 InstanceLockBase * _pObj;
 public :
  InstanceLock(InstanceLockBase * pObj)
  ... {
   _pObj = pObj; // 这里会保存一份指向s的指针,用于解锁
   if (NULL != _pObj)
   _pObj -> Lock(); // 这里加锁
  }
  ~ InstanceLock()
  ... {
   if (NULL != _pObj)
   _pObj -> Unlock(); // 这里解锁
 }
} ;

  最后别忘了在类InstanceLockBase中把InstanceLock声明为友元,使得它能正确访问Lock和Unlock这两个私有函数:

class InstanceLockBase
... {
 friend class InstanceLock;
 …
} ;

  好了,有了上面的基础,现在对变量s的加解锁管理变成了对InstanceLock的实例的生命周期的管理了。假如我们有一个函数ModifyS中要对s进行修改,那么只要在函数一开始就声明一个InstaceLock的实例,这样整个函数就自动对s加锁,一旦进入这个函数,其他线程就都不能获得s的锁了:

void ModifyS()
... {
 InstanceLock lock ( & s); // 这里已经实现加锁了
 // some operations on s
} // 一旦离开lock对象的作用域,自动解锁

  如果是要对某函数中一部分代码加锁,只要用一对大括号把它们括起来再声明一个lock就可以了:


... {
 InstanceLock lock ( & s);
 // do something …
}

  好了,就是这么简单。下面来看一个测试。

  首先准备一个输出函数,对我们理解程序有帮助。它会在输出我们想输出的内容同时打出行号和时间:

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

  当然,原则上当多线程都调用这个函数时应该对其静态局部变量count进行加锁,这里就省略了。

  我们声明一个非常简单的被锁的类型,并生成一个实例:

class MyClass: public InstanceLockBase
... {} ;
MyClass mc;

  子线程的任务就是对这个对象加锁,然后输出一些信息:

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

  这里会输出两条信息,一是在刚刚获得锁的时间,二是在释放锁的时候,中间通过Sleep来延迟2秒。

  主线程负责开启子线程,然后也对mc加锁:

CreateThread( 0 , 0 , ThreadProc, 0 , 0 , 0 );
... {
 InstanceLock il( & mc);
 Say( " in main thread, lock " );
 Sleep( 3000 );
 Say( " in main thread, lock " );
}

  运行此程序,得到的输出如下:

001 [13:43:23.781]in main thread, lock
002 [13:43:26.781]in main thread, lock
003 [13:43:26.781]in sub thread, lock
004 [13:43:28.781]in sub thread, unlock

  从其输出的行号和时间可以清楚的看到两个线程间的互斥:当主线程恰好首先获得锁时,它会延迟3秒,然后释放锁,之后子线程才得以继续进行。这个例子也证明我们的类工作的很好。

  总结一下,要使用InstanceLock系列类,要做的就是:

  1、让被锁类从InstanceLockBase继承

  2、所有要访问被锁对象的代码前面声明InstanceLock的实例,并传入被锁对象的指针。

  附:完整源代码:

#pragma once
#include < windows.h >

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);
 }
} ;

class InstanceLock
... {
 InstanceLockBase * _pObj;
 public :
  InstanceLock(InstanceLockBase * pObj)
  ... {
   _pObj = pObj;
   if (NULL != _pObj)
    _pObj -> Lock();
  }

 ~ InstanceLock()
 ... {
  if (NULL != _pObj)
   _pObj -> Unlock();
 }
} ;

 

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值