1、多线程操作共享数据带来的问题
对于一条自增语句 i++,实际上是执行三条汇编指令:①、先将i的值放到寄存器中,②、在寄存器中将i的值加1,③、将寄存器中i的值赋给内存i中。
现在如果有两个线程同时执行i++的话,最好的情况就是一个线程执行完①,②,③后,另一个线程再执行①,②,③步。但线程很有可能不是这样顺序执行,这种情况下就可能出现访问计算错误:假设i的初始值为0,线程1执行完②后,线程2开始执行并执行完③,这时候i的值为1,线程1接着运行完③,所以最后i的值还是为1 。
上面是两个线程都对共享数据进行写入的情况产生的竞争问题,当一个线程写入,另一个线程读取的时候也可能会产生业务上的问题,比如经典的购票问题:北京站和上海站共享一个订票数据库,假设当前剩余票数count为1,有人在北京站购买了一张票,剩余票数就应该减1:count--,而如果在count--执行到汇编的第③步之前的时候,上海站有人查询剩余票数是有票的,而当他去购买的时候就会发现票已经是0了,因为这时北京那边的count--已经完成,这就给客户造成了困扰。
Windows线程同步方法主要有互斥对象、事件对象、信标对象、关键代码段,我们可以根据其同步的不同特点来使用。
2、互斥量
另外,当线程对共享资源访问结束后,应释放对互斥对象的所有权,这时需要调用ReleaseMutex()函数,函数原型:
函数参数为要释放的互斥对象的句柄,函数执行成功返回TRUE,否则返回FALSE。需要注意的是,如果同一个线程多次请求了同一个互斥对象,那么也应对应的多次调用ReleaseMutex()释放所有权。还有就是
还可以利用互斥对象来实现保证应用程序只有一个实例在运行:
以下为使用互斥对象实现购票系统线程同步的例子:
#include <stdio.h> #include <tchar.h> #include <windows.h> #include <iostream> using std::cin; using std::cout; using std::endl; DWORD WINAPI Thread1Proc(LPVOID lpParameter); DWORD WINAPI Thread2Proc(LPVOID lpParameter); int tickets = 100;//全局剩余票数 HANDLE g_hMutex;//全局互斥对象句柄 int main() { HANDLE hThread1, hThread2; g_hMutex = CreateMutex(NULL, FALSE, _T("ticket"));//创建互斥对象 if(g_hMutex) { if(ERROR_ALREADY_EXISTS == GetLastError())//保证应用程序只有一个实例运行 { cout << "Only one instance an run!" << endl; return 0; } } hThread1 = CreateThread(NULL, 0, Thread1Proc, NULL, 0, NULL);//线程1 hThread2 = CreateThread(NULL, 0, Thread2Proc, NULL, 0, NULL);//线程2 CloseHandle(hThread1); CloseHandle(hThread2); Sleep(40000); CloseHandle(g_hMutex); return 0; } DWORD WINAPI Thread1Proc(LPVOID lpParameter) { while(1) { WaitForSingleObject(g_hMutex, INFINITE);//等待互斥对象有信号,获得该对象的所有权 if(tickets > 0) { Sleep(1); cout << "thread1 sell ticket:" << tickets-- << endl; } else { break; } ReleaseMutex(g_hMutex);//释放互斥对象的所有权 } return 0; } DWORD WINAPI Thread2Proc(LPVOID lpParameter) { while(1) { WaitForSingleObject(g_hMutex, INFINITE);//等待互斥对象有信号,获得该对象的所有权 if(tickets > 0) { Sleep(1); cout << "thread2 sell ticket:" << tickets-- << endl; } else { break; } ReleaseMutex(g_hMutex);//释放互斥对象所有权 } return 0; }
3、事件对象
事件对象也属于内核对象,事件对象是用来通知其他进程/线程某个操作已经开始或完成,在线程访问某一资源之前,也许需要等待某一事件的发生,这时用事件对象最合适。线程可以通过事件对象获知某件事件是否发生,从而确定是否开始执行任务。
事件对象的使用一般遵循如下过程:首先,定义一个句柄型变量用来保存创建的事件对象;调用CreateEvent函数创建一个事件对象,该函数返回一个事件句柄;然后,可以设置(SetEvent)或者复位(ResetEvent)一个事件对象,也可以发一个事件脉冲(PlusEvent),即设置一个事件对象,然后复位它。
CreateEvent()用来创建或打开一个事件对象,函数原型:
HANDLE WINAPI CreateEvent(
_In_opt_ LPSECURITY_ATTRIBUTES lpEventAttributes,
_In_ BOOL bManualReset,
_In_ BOOL bInitialState,
_In_opt_ LPCTSTR lpName
);
其它线程调用WaitForSingleObject()来等待事件对象,获得事件对象使用权后事件对象变为无信号状态。
SetEvent(HANDLE hEvent)将指定的事件对象设置为有信号状态。
PulseEvent(HANDLE hEvent)在自动重置的事件对象下与SetEvent()功能呢相同,如果是人工重置的事件对象则在其它线程在WaitForSingleObject()等待到事件对象变为有信号后会自动将事件对象置为无信号,所以相当于又将这个事件对象设置成了自动重置的类型。
ResetEvent(HANDLE hEvent)将指定的事件对象设置为无信号状态。
《Windows核心编程》上的一个使用人工重置事件对象的示例:
主线程创建一个人工重置的未通知状态的事件对象,将句柄保存在一个全局变量中,然后创建三个线程。这些线程要等待数据已准备好,然后每个线程要只读访问这些数据。一旦主线程将数据准备好,它就调用SetEvent()给事件对象发出通知信号,而这时三个线程都会进入可调度状态,可以访问数据了,如果有3个CPU的话,这三个线程就可以真正的同时访问这个数据,效率非常高。
《Windows核心编程》上的一个使用自动重置事件对象的示例:
同样可以利用事件对象实现应用程序只有一个实例运行,以下代码为使用事件对象实现购票系统线程同步的例子:
#include <stdio.h> #include <tchar.h> #include <windows.h> #include <iostream> using std::cin; using std::cout; using std::endl; int g_iTickets = 100;//全局剩余票数 HANDLE g_hEvent;//全局事件对象句柄 int main() { HANDLE hThread1, hThread2; g_hEvent = CreateEvent(NULL, FALSE, TRUE, _T("ticket"));//创建自动重置的事件对象 if(g_hEvent) { if(ERROR_ALREADY_EXISTS == GetLastError())//保证应用程序只有一个实例运行 { cout << "Only one instance can run!" << endl; return 0; } } hThread1 = CreateThread(NULL, 0, Thread1Proc, NULL, 0, NULL);//线程1 hThread2 = CreateThread(NULL, 0, Thread2Proc, NULL, 0, NULL);//线程2 CloseHandle(hThread1); CloseHandle(hThread2); Sleep(40000); CloseHandle(g_hEvent); return 0; } DWORD WINAPI Thread1Proc(LPVOID lpParameter) { while(1) { WaitForSingleObject(g_hEvent, INFINITE);//请求事件对象 if(g_iTickets > 0) { Sleep(1); cout << "thread1 sell ticket:" << g_iTickets-- << endl; } else { break; } SetEvent(g_hEvent);//设置事件对象为有信号状态 } return 0; } DWORD WINAPI Thread2Proc(LPVOID lpParameter) { while(1) { WaitForSingleObject(g_hEvent, INFINITE);//请求事件对象 if(g_iTickets > 0) { Sleep(1); cout << "thread2 sell ticket:" << g_iTickets-- << endl; } else { break; } SetEvent(g_hEvent);//设置事件对象为有信号状态 } return 0; }为了实现线程间同步,不应使用人工重置的事件对象,应该使用自动重置的事件对象。而如果各线程是只读访问共享资源的,则可以使用人工重置的事件对象,效率更高。
4、关键代码段与旋转锁
关键代码段又称临界区,通常把多线程中访问共享资源的那段代码当做关键代码段。
InitializeCriticalSection(LPCRITICAL_SECTION lpCriticalSection)用来初始化临界区,参数lpCriticalSection为指向CRITICAL_SECTION结构的指针,作为返回值来使用。
EnterCriticalSection(LPCRITICAL_SECTION lpCriticalSection)用来获得临界区对象使用权。
TryEnterCriticalSection(LPCRITICAL_SECTION lpCriticalSection):和EnterCriticalSection不同点在于TryEnterCriticalSection是无论获得临界区成功还是失败都立即返回,如果失败则返回0,否则返回非0值。
LeaveCriticalSection(LPCRITICAL_SECTION lpCriticalSection)用来释放临界区对象使用权。
DeleteCriticalSection(LPCRITICAL_SECTION lpCriticalSection)用来释放临界区对象。
以下代码为使用关键代码段实现购票系统线程同步的例子:
#include <stdio.h> #include <tchar.h> #include <windows.h> #include <iostream> using std::cin; using std::cout; using std::endl; int g_iTickets = 100;//全局剩余票数 CRITICAL_SECTION g_cs;//全局临界区 int main() { HANDLE hThread1, hThread2; InitializeCriticalSection(&g_cs);//初始化临界区 hThread1 = CreateThread(NULL, 0, Thread1Proc, NULL, 0, NULL);//线程1 hThread2 = CreateThread(NULL, 0, Thread2Proc, NULL, 0, NULL);//线程2 CloseHandle(hThread1); CloseHandle(hThread2); Sleep(40000); DeleteCriticalSection(&g_cs);//释放临界区 return 0; } DWORD WINAPI Thread1Proc(LPVOID lpParameter) { while(1) { EnterCriticalSection(&g_cs);//获得临界区对象使用权 if(g_iTickets > 0) { Sleep(1); cout << "thread1 sell ticket:" << g_iTickets-- << endl; } else { break; } LeaveCriticalSection(&g_cs);//释放临界区对象使用权 } return 0; } DWORD WINAPI Thread2Proc(LPVOID lpParameter) { while(1) { EnterCriticalSection(&g_cs);//获得临界区对象使用权 if(g_iTickets > 0) { Sleep(1); cout << "thread2 sell ticket:" << g_iTickets-- << endl; } else { break; } LeaveCriticalSection(&g_cs);//释放临界区对象使用权 } return 0; }
旋转锁其实就是关键代码段的升级,与关键代码段不同的是如果线程获得关键代码段使用权失败的话它会进入一个循环,就像在自我旋转一样,直到获得关键代码段使用权或指定的循环次数已到。
InitializeCriticalSectionAndSpinCount(LPCRITICAL_SECTION lpCriticalSection, DWORD dwSpinCount)函数用来初始化旋转锁,参数dwSpinCount用来指定旋转次数,一个参考值是4000。旋转锁使用的其它函数则与关键代码段相同。
在单CPU电脑上应避免使用旋转锁,因为线程不停的循环会导致其独占CPU,其它线程则无法获得CPU来改变锁的使用。
另外需要注意的是,关键代码段和其所保护的资源数据最好位于不同的高速缓存行。如果二者在同一高速缓存行的话,如果当前获得关键代码段的线程修改了资源数据,这会使其它CPU线程的同一高速缓存行失效从而重新从内存更新高速缓存行数据,即这时候等待关键代码段的线程要从内存中重新读入关键代码段。如果二者不在同一高速缓存行的话,则等待关键代码段的线程就不用重新从内存读取数据。
5、信号量
信号量又称信标对象,它也属于内核对象。信标对象作为同步对象,它的最大特点是允许多个线程同时使用同一资源。信标对象同互斥对象一样都是为了保护全局变量的安全性,不同的是,互斥对象只有一把钥匙,也就是同一时刻只有一个线程可以访问它,只有这个线程释放它,其他的线程才有机会拿到它,并且线程结束,互斥对象被释放;而信标对象可以有很多钥匙,也就是说可以设定多个线程来访问它,并且线程结束,信标对象不释放。例如,假设我们创建信标对象时,提供了三把钥匙,主线程拿到一个,即使它不释放,还可以有两个线程可以拿到信标对象,然而若再想别的线程拿到信标对象,则至少要有一把钥匙被释放。
信标对象维护一个从0开始的计数,在计数值大于0时,表示对象是有信号的,而在计数值为0时,则是无信号的。信标对象可用来限制对共享资源进行访问的线程数量。线程用CreateSemaphore函数来建立信标对象,在调用该函数时,可以指定对象的初始计数和最大计数。在建立信标对象时也可以为对象起个名字,别的进程中的线程可以用OpenSemaphore函数打开指定名字的信标对象句柄。
一般把信标对象的初始计数设置成最大值。每次当信标对象有信号使等待函数返回时,信标对象计数就会减1,当信标对象计数为0时,表示信标对象都被拿去了;而调用ReleaseSemaphore可以增加信标对象的计数。计数值越大,表明可供拿走的信标对象越多,当前访问共享资源的线程越少;计数值越小就表明可供拿走的信标对象变少,当前访问共享资源的线程越多。
《windows核心编程》上使用信标内核对象的一个示例:
比如一个服务器程序,对于存放客户请求的缓冲区大小我们设置成5,即每次最多能存放5个客户请求,如果第5个客户请求尚未处理完毕,则新的客户连接请求会被拒绝。在服务器进程里,我们创建5个线程,每个线程处理一个客户请求。刚开始时,没有客户请求,则5个线程都不应成为可调度线程,如果有3个客户请求同时到来,则应该有三个线程处于可调度状态,使用信标对象则可以满足这些要求:一开始将信标对象最大资源数设为5,将当前资源数设为0,因为还没有客户请求,当客户请求接受时,就将当前资源数递增,当客户请求被提交给线程进行处理时,当前资源数量递减。
下面将介绍信标对象的几个相关的函数:
1. HANDLE CreateSemaphore(
LPSECURITY_ATTRIBUTES lpSemaphoreAttributes,
LONG lInitialCount,
LONG lMaximumCount,
LPCTSTR lpName
);
该函数用于创建一个信标对象。调用该函数若创建成功,则返回信标对象句柄;若要创建的命名信标对象已经存在,则返回已存在的信标对象的句柄;若调用该函数返回NULL,则表示创建失败。
第一个参数lpSemaphoreAttributes,表示安全属性,NULL表示使用默认的安全描述。
第二个参数lInitialCount,表示创建的信标对象的初始计数值。该参数必须大于等于0且小于等于第三个参数的值。当参数lInitialCount大于0时,表示信标对象有信号状态;当lInitialCount等于0时,表示信标对象处于无信号状态。
第三个参数lMaximumCount,表示创建的信标对象的最大计数值。该参数必须大于0。
第四个参数lpName,表示创建的信标对象的名字。可以为NULL,表示创建匿名的。
2. HANDLE OpenSemaphore (
DWORD dwDesiredAccess,
BOOL bInheritHandle,
LPCTSTR lpName
);
该函数用于打开一个存在的信标对象。该函数调用成功返回打开的信标对象的句柄;如果调用失败,则返回NULL。
第一个参数dwDesiredAccess,表示对信标对象的存取方式。
第二个参数bInheritHandle,指定创建的信标对象是否可以被继承。如果该参数为TRUE,则表示可以被继承;否则不可以。
第三个参数名字lpName,表示要打开的信标对象的名字,该参数区分大小写。
3. BOOL ReleaseSemaphore(
HANDLE hSemaphore,
LONG lReleaseCount,
LPLONG lpPreviousCount
);
该函数用于释放信标对象。函数调用成功返回非0值。
第一个参数hSemaphore,表示信标对象句柄。
第二个参数lReleaseCount,表示释放的数目,让信标对象值增加的数目。
第三个参数lpPreviousCount,用来得到释放前信标对象的计数值,可以为NULL。
信标对象的使用一般遵循如下过程:首先,定义一个句柄型变量用来保存创建的信标对象;然后,调用CreateSemaphore函数创建一个信标对象;然后,调用WaitForSingleObject等待函数来获取信标对象,可以的话利用关键资源;最后,调用RealseSemaphore释放信标对象。
以下代码为使用信标实现购票系统线程同步的例子:
#include <stdio.h> #include <tchar.h> #include <windows.h> #include <iostream> using std::cin; using std::cout; using std::endl; int g_iTickets = 100;//全局剩余票数 HANDLE g_hSem;//全局信标对象句柄 int _tmain(int argc, _TCHAR* argv[]) { HANDLE hThread1, hThread2; g_hSem = CreateSemaphore(NULL, 1, 3, L"mysemaphore");//创建信标对象,初始计数值为1,最大计数值为3 hThread1 = CreateThread(NULL, 0, Thread1Proc, NULL, 0, NULL);//线程1 hThread2 = CreateThread(NULL, 0, Thread2Proc, NULL, 0, NULL);//线程2 CloseHandle(hThread1); CloseHandle(hThread2); Sleep(40000); CloseHandle(g_hSem); return 0; } DWORD WINAPI Thread1Proc(LPVOID lpParameter) { while(1) { WaitForSingleObject(g_hSem, INFINITE);//请求信标对象 if(g_iTickets > 0) { Sleep(1); cout << "thread1 sell ticket:" << g_iTickets-- << endl; } else { break; } ReleaseSemaphore(g_hSem,1,NULL);//释放信标对象 } return 0; } DWORD WINAPI Thread2Proc(LPVOID lpParameter) { while(1) { WaitForSingleObject(g_hSem, INFINITE);//请求信标对象 if(g_iTickets > 0) { Sleep(1); cout << "thread2 sell ticket:" << g_iTickets-- << endl; } else { break; } ReleaseSemaphore(g_hSem,1,NULL);//释放信标对象 } return 0; }
6、可等待计时器对象
可等待计时器对象可以设置在某个指定的时间被触发,或每隔一段时间触发一次,它也属于内核对象。一般我们在一个线程中创建和设置好可等待计时器对象后,其它线程就可以调用WaitForSingleObject函数等待计时器对象的触发。
创建可等待计时器对象使用CreateWaitableTimer(),函数返回值创建的计时器对象的句柄:
HANDLE WINAPI CreateWaitableTimer(LPSECURITY_ATTRIBUTES lpTimerAttributes, /*安全属性*/BOOL bManualReset, /*指定人工重置TRUE或自动重置FALSE*/LPCTSTR lpTimerName /*对象名称*/);
多个线程可以等待一个可等待计时器对象,人工重置的计时器对象在触发后所有等待线程都变成可调度状态,自动重置的计时器对象在触发后只有一个等待线程变为可调度状态。
SetWaitableTimer()用来设置可等待计时器第一次被触发时间及以后的触发间隔,它也可以用来重置一个计时器:BOOL WINAPI SetWaitableTimer( _In_ HANDLE hTimer, /*可等待计时器*/ _In_ const LARGE_INTEGER *pDueTime, /*第一次被触发的时间*/ _In_ LONG lPeriod, /*触发间隔,只触发一次则传0*/ _In_opt_ PTIMERAPCROUTINE pfnCompletionRoutine, /*同时触发的异步过程调用*/ _In_opt_ LPVOID lpArgToCompletionRoutine/*异步过程调用参数*/, _In_ BOOL fResume/*一般传入FALSE*/);
暂停可等待计时器的触发:CancelWaitableTimer()关闭可等待计时器内核对象同样使用CloseHandle()。
7,读写锁SRWLock读写锁可以区分想要读取资源值的线程和想要写入资源的线程,它允许所有的读取者线程在同一时刻访问共享资源,这是因为读取资源并不存在破坏资源的风险,只有当写入者线程想要对资源进行更新的时候才需要进行同步:让其它写入者线程和读取者线程等待,在这种情况下,写入者线程独占对资源的访问权。读写锁的主要函数就五个:初始化函数、写入者线程申请、释放函数,读取者线程申请、释放函数。VOID InitializeSRWLock(PSRWLOCK SRWLock); //初始化读写锁VOID AcquireSRWLockExclusive(PSRWLOCK SRWLock); //写入线程申请获得对被保护资源的独占访问权VOID ReleaseSRWLockExclusive(PSRWLOCK SRWLock); //写入线程释放对资源的占用VOID AcquireSRWLockShared(PSRWLOCK SRWLock); //读取线程申请获得对被保护资源的访问权VOID ReleaseSRWLockShared(PSRWLOCK SRWLock); //读取线程释放对资源的占用
8、条件变量
Windows下的条件变量与Linux下条件变量用法和功能大体相同,都额外需要一个锁,Windows下这个锁可以为一个关键代码段或读写锁。
InitializeConditionVariable()用来初始化一个条件变量。SleepConditionVariableCS()/SleepConditionVariableSRW()用来等待条件变量。WakeConditionVariable()用来唤醒(触发)等待的条件变量。
下面是使用条件变量的一个示例:
#include <stdio.h> #include <Windows.h> CONDITION_VARIABLE g_cd;//条件变量 CRITICAL_SECTION g_cs;//临界区 bool g_bConditionFlag = false; DWORD WINAPI Thread1Proc(LPVOID lpParameter) { EnterCriticalSection(&g_cs); while (!g_bConditionFlag) { Sleep(1000); SleepConditionVariableCS(&g_cd, &g_cs, INFINITE); } LeaveCriticalSection(&g_cs); printf("thread1 exit\n"); return 0; } void main() { InitializeCriticalSection(&g_cs);//初始化临界区 InitializeConditionVariable(&g_cd);//初始化条件变量 HANDLE hThread1 = CreateThread(NULL, 0, Thread1Proc, NULL, 0, NULL);//线程1 CloseHandle(hThread1); Sleep(100); EnterCriticalSection(&g_cs); g_bConditionFlag = true; WakeConditionVariable(&g_cd); LeaveCriticalSection(&g_cs); printf("here\n"); getchar(); DeleteCriticalSection(&g_cs);//释放临界区 }
9、Interlocked系列函数及C++11中的atomic_int、atomic_llong、atomic_flag
interlockedincrement()系列函数类似C++11中的atomic_int、atomic_llong、atomic_flag实现的原子操作数值的功能。
interlockedincrement():原子加1
InterlockedDecrement():原子减1
InterlockedExchangeAdd():原子加法
InterlockedExchangeSubtract():原子减法
InterlockedExchange():原子获得值后再重新赋值
InterlockedExchangePointer():原子替换指针
LONG __cdecl InterlockedCompareExchange(
LONG volatile *Destination,
LONG Exchange,
LONG Comparand
);:将Dest和comp进行比较,如果相等则将Dest赋值为exchange,否则保持不变。
InterlockedCompareExchangePointer():对指针进行比较和赋值操作。
还有针对LONGLONG类型的Interlocked系列函数,如InterlockedExchange64()。
10、SignalObjectAndWait()函数
SignalObjectAndWait()函数会触发一个内核对象(互斥锁、信号量、事件对象)并等待另一个内核对象(互斥锁、信号量、事件对象、计时器、进程、线程、作业、控制台输入等),并且这两个操作是一个原子操作。
SignalObjectAndWait使一个函数完成了两个操作,节省了时间,比如以下代码会频繁在用户态和内核态之间切换,花费了CPU周期,而用SignalObjectAndWait可以把这两个操作合并从而减少处理时间:
ReleaseMutex(hMutex);
WaitForSingleObject(hEvent, INFINITE);
SignalObjectAndWait的原子性操作也避免了一些问题:如以下代码,线程1设置事件对象2为有信号后线程2受信,如果线程2中SetEvent(hEvent1)发生在线程1的WaitForSingleObject(hEvent1, INFINITE)之前的话则线程1会错过受信。
Thread1:
SetEvent(hEvent2);
WaitForSingleObject(hEvent1, INFINITE);
Thread2:
WaitForSingleObject(hEvent2, INFINITE);
SetEvent(hEvent1);
如果线程1中使用SignalObjectAndWait()这个原子操作的话就不会出现这种情况。
10、volatile
volatile用来禁止编译器优化,以防止对变量或对象的访问都是从寄存器而不是内存读取,C++11中明确表示volatile只用于硬件访问,而不是线程间同步。
11、同步方法的选用
互斥量、事件对象、信号量、可等待计时器对象顿号、SignalObjectAndWait()函数都属于内核对象或使用内核对象来实现,同步速度较慢,但可以跨进程进行线程同步;关键代码段、读写锁、条件变量、atomic_int、atomic_llong、
Interlocked系列函数工作在用户模式下,同步速度较快,但只能在同一进程下的线程间进行同步。
读写锁和条件变量运行的最低系统为Windows Vista和Windows Server 2008。
12、线程同步的技巧(以关键代码段为例)
第一:操作一组逻辑相关的资源时,使用一个锁
例如我们向一个集合添加或删除元素的时候,可能同时需要对一个计数器进行更新,那么操作这些逻辑相关的资源时使用一个锁。
第二:若有多个逻辑上互不相关的共享资源则每个资源使用一个CRITICAL_SECTION。
例如,以下ThreadProc1中循环运行时,没有一个线程能够访问任何一个数组,而这两个数组之间毫无关系:
int g_nNums[100]; TCHAR g_cChars[100]; CRITICAL_SECTION g_cs; DWORD WINAPI ThreadProc1(LPVOID lpParameter) { EnterCriticalSection(&g_cs); for(int i=0; i<100; i++) { g_nNums[i] = 0; g_cChars[i] = _T('x'); } LeaveCriticalSection(&g_cs); return 0; }可以进行以下改进,使ThreadProc1一旦完成g_nNums[]的初始化后,其它线程就可以开始访问g_nNums[]数组。
int g_nNums[100]; CRITICAL_SECTION g_csNums; TCHAR g_cChars[100]; CRITICAL_SECTION g_csChars; DWORD WINAPI ThreadProc1(LPVOID lpParameter) { EnterCriticalSection(&g_csNums); for(int i=0; i<100; i++) g_nNums[i] = 0; LeaveCriticalSection(&g_csNums); EnterCriticalSection(&g_csChars); for(int i=0; i<100; i++) g_cChars[i] = _T('x'); LeaveCriticalSection(&g_csChars); return 0; }
第三:若要访问多个共享资源,则每个线程按照相同的顺序对资源进行请求。
例如以下ThreadProc1与ThreadProc2就容易产生死锁,因为当ThreadProc1线程运行到EnterCriticalSection(&g_csNums)后,若CPU时间到期,转到ThreadProc2线程运行,当运行到EnterCriticalSection(&g_csChars)时,CPU时间到期,又回到ThreadProc1线程,则此时就会产生死锁现象。
DWORD WINAPI ThreadProc1(LPVOID lpParameter) { EnterCriticalSection(&g_csNums); EnterCriticalSection(&g_csChars); for(int i=0; i<100; i++) g_nNums[i] = g_cChars[i]; LeaveCriticalSection(&g_csChars); LeaveCriticalSection(&g_csNums); return 0; } DWORD WINAPI ThreadProc2(LPVOID lpParameter) { EnterCriticalSection(&g_csChars); EnterCriticalSection(&g_csNums); for(int i=0; i<100; i++) g_nNums[i] = g_cChars[i]; LeaveCriticalSection(&g_csChars); LeaveCriticalSection(&g_csNums); return 0; }解决方法为使两个线程请求多个资源的顺序相同。而释放使用权的顺序则没有关系,因为释放不会使线程进入等待状态。
第四:不要长时间占用锁
例如以下代码中的SendMessage()函数为阻塞函数,其运行时间不确定,而其他线程则可能很久才能获得g_somestruct使用权:
int g_iNum; CRITICAL_SECTION g_cs; DWORD WINAPI ThreadProc1(LPVOID lpParameter) { EnterCriticalSection(&g_cs); SendMessage(hWnd, WM_MyMessage, &g_iNum, 0); LeaveCriticalSection(&g_cs); return 0; }可以进行以下改进:
int g_iNum; CRITICAL_SECTION g_cs; DWORD WINAPI ThreadProc1(LPVOID lpParameter) { EnterCriticalSection(&g_cs); int iNumTemp = g_iNum; LeaveCriticalSection(&g_cs); SendMessage(hWnd, WM_MyMessage, &iNumTemp, 0); return 0; }