线程同步
同步可以保证在一个时间内只有一个线程对某个共享资源有控制权。共享资源包括全局变量、公共数据成员或者句柄等。临界区内核对象和事件内核对象可以很好地用于多线程同步和它们之间的通信。
临界区对象
为什么要线程同步
当多个线程在同一个进程中执行时,可能有不止一个线程同时执行同一段代码,访问同一段内存中的数据。多个线程同时读共享数据没有问题,但如果同时读和写,情况就不同了。下面是一个有问题的程序,该程序用两个线程来同时增加全局变量 g_nCount1 和 g_nCount2 的计数,运行 1 秒之后打印出计数结果。
CountErr.cpp
#include <stdio.h>
#include <windows.h>
#include <process.h>
int g_nCount1 = 0;
int g_nCount2 = 0;
BOOL g_bContinue = TRUE;
UINT __stdcall ThreadFunc(LPVOID);
int main(int argc, char* argv[])
{
UINT uId;
HANDLE h[2];
h[0] = (HANDLE)::_beginthreadex(NULL, 0, ThreadFunc, NULL, 0, &uId);
h[1] = (HANDLE)::_beginthreadex(NULL, 0, ThreadFunc, NULL, 0, &uId);
// 等待1秒后通知两个计数线程结束,关闭句柄
Sleep(1000);
g_bContinue = FALSE;
::WaitForMultipleObjects(2, h, TRUE, INFINITE);
::CloseHandle(h[0]);
::CloseHandle(h[1]);
printf("g_nCount1 = %d \n", g_nCount1);
printf("g_nCount2 = %d \n", g_nCount2);
return 0;
}
UINT __stdcall ThreadFunc(LPVOID)
{
while(g_bContinue)
{
g_nCount1++;
g_nCount2++;
}
return 0;
}
线程函数 ThreadFunc 同时增加全局变量 g_nCount1 和 g_nCount2 的计数。按道理来说最终在主线程中输出的它们的值应该是相同的,可是结果并不尽如人意,图所示是运行上面的代码,并等待 1 秒后程序的输出。
g_nCount1 和 g_nCount2 的值并不相同。出现这种结果主要是因为同时访问 g_nCount1 和g_nCount2 的两个线程具有相同的优先级。在执行过程中如果第一个线程取走 g_nCount1 的值准备进行自加操作的时候,它的时间片恰好用完,系统切换到第二个线程去对 g_nCount1 进行自加操作;一个时间片过后,第一个线程再次被调度,此时它会将上次取出的值自加,并放入g_nCount1 所在的内存里,这就会覆盖掉第二个线程对 g_nCount1 的自加操作。变量 g_nCount2也存在相同的问题。由于这样的事情的发生次数是不可预知的,所以最终的值就不相同了。
例子中,g_nCount1 和 g_nCount2 是全局变量,属于该进程内所有线程共有的资源。多线程同步就要保证在一个线程占有公共资源的时候,其他线程不会再次占有这个资源。所以,解决同步问题,就是保证整个存取过程的独占性。在一个线程对某个对象进行操作的过程中,需要有某种机制阻止其他线程的操作,这就用到了临界区对象。
使用临界区对象
临界区对象是定义在数据段中的一个 CRITICAL_SECTION 结构,Windows 内部使用这个结构记录一些信息,确保在同一时间只有一个线程访问该数据段中的数据。
编程的时候,要把临界区对象定义在想保护的数据段中,然后在任何线程使用此临界区对象之前对它进行初始化。
void InitializeCriticalSection(LPCRITICAL_SECTION lpCriticalSection );
// 指向数据段中定义的CRITICAL_SECTION 结构
之后,线程访问临界区中数据的时候,必须首先调用 EnterCriticalSection 函数,申请进入临界区(又叫关键代码段)。在同一时间内,Windows 只允许一个线程进入临界区。所以在申请的时候,如果有另一个线程在临界区的话,EnterCriticalSection 函数会一直等待下去,直到其他线程离开临界区才返回。EnterCriticalSection 函数用法如下。
void EnterCriticalSection( LPCRITICAL_SECTION lpCriticalSection);
当操作完成的时候,还要将临界区交还给 Windows,以便其他线程可以申请使用。这个工作由 LeaveCriticalSection 函数来完成。
void LeaveCriticalSection( LPCRITICAL_SECTION lpCriticalSection);
当程序不再使用临界区对象的时候,必须使用 DeleteCriticalSection 函数将它删除。
void DeleteCriticalSection( LPCRITICAL_SECTION lpCriticalSection);
现在使用临界区对象来改写上面有同步问题的计数程序。