线程同步之临界区
临界区(Critical Section)指每个进程中访问临界资源的那段代码,临界资源是一次仅允许一个进程使用,多个进程中涉及到同一个临界资源的临界区称为相关临界区。
临界区线程同步原理:
有多个线程试图同时访问临界区,那么在有一个线程进入后其他所有试图访问此临界区的线程将被挂起,并一直持续到进入临界区的线程离开。临界区在被释放后,其他线程可以继续抢占,并以此达到用原子方式操作共享资源的目的。
临界区在使用时以CRITICAL_SECTION结构对象保护共享资源,并分别用EnterCriticalSection()和LeaveCriticalSection()函数去标识和释放一个临界区。
所用到的CRITICAL_SECTION结构对象必须经过InitializeCriticalSection()的初始化后才能使用,而且必须确保所有线程中的任何试图访问此共享资源的代码都处在此临界区的保护之下。否则临界区将不会起到应有的作用,共享资源依然有被破坏的可能。
说明:
分别利用多线程和单线程对一个全局变量进行累加,累加到5亿。同时计时观察两者所使用的时间。
多线程:共有5条线程,每条线程负责累加1亿次。
单线程:利用for(),反复调用函数。
注意:为了客观性,不是for()中直接对全局变量累加,因为线程的创建和调用也需要时间。所以此处单线程也采用函数调用的方式。
计时方法:
方法一:
int nStart=GetTickCount();
int nTime = (GetTickCount() - nStart);
printf("TickTime:%d\n", nTime);
此种方法是系统时间计数(毫秒),精度非最高。
方法二:
LARGE_INTEGER liFre,liStart,liEnd;
QueryPerformanceFrequency(&liFre); //返回定时器的频率。
QueryPerformanceCounter(&liEnd);
LONGLONG llTime = (liEnd.QuadPart - liStart.QuadPart)*1000 / (int)liFre.QuadPart;
printf("QueryPerformance: %d\n", llTime);
此种方法是系统定时器频率计数(秒),精度较高。
比如:以Sleep(1000)为例,第一方法返回的时间是999,第二方法返回的时间是997。
临界区的配套使用:
//临界区
CRITICAL_SECTION g_cs;
//初始化临界区
::InitializeCriticalSection(&g_cs);
//进入临界区
::EnterCriticalSection(&g_cs);
。。。。在此处进行同步操作(读、写)。。。。
//离开临界区
::LeaveCriticalSection(&g_cs);
//释放临界区
::DeleteCriticalSection(&g_cs);
注意注意:在程序中出现这么一段代码(情况一)
情况一:
for (int i = 0; i < 100000000; ++i)
{
//进入临界区
::EnterCriticalSection(&g_cs);
g_nCount++;
//离开临界区
::LeaveCriticalSection(&g_cs);
}
情况二:
//进入临界区
::EnterCriticalSection(&g_cs);
for (int i = 0; i < 100000000; ++i)
{
g_nCount++;
}
//离开临界区
::LeaveCriticalSection(&g_cs);
区别:两者是有区别,而且区别很大。
首先,情况一g_nCount++才属于临界区内容,即for()可以不断被执行,同样也可以被其他线程使用。
然后,情况的for()属于临界区内容,即进入临界区后,其他的线程无法在使用for,此时其他线程处于等待状态,
但是,一旦g_nCount++操作出现阻塞,那么是不是也意味着其他线程一直处于等待状态?此时的效率就大大地降低了。
所以:一般临界区的内容,都是直接写、读,不用于其他操作。避免造成资源浪费。
固有特点(优点+缺点):
1、是一个用户模式的对象,不是系统核心对象;
2、因为不是核心对象,所以执行速度快,有效率;
3、因为不是核心对象,所以不能跨进程使用;
4、可以多次“进入”,但必须多次“退出”;
5、最好不要同时进入或等待多个 Critical Sections,容易造成死锁;
6、无法检测到进入到 Critical Sections 里面的线程当前是否已经退出!
源代码: #include "stdafx.h" #include <Windows.h> #include <process.h> //线程数 #define g_nThreadNum 5 //临界区 CRITICAL_SECTION g_cs; //累加数 int g_nCount = 0; //多线程将g_nCount加到5亿 unsigned _stdcall ThreadFunc(void * lParam) { //每一个线程负责加1亿 for (int i = 0; i < 100000000; ++i) { //进入临界区 ::EnterCriticalSection(&g_cs); g_nCount++; //离开临界区 ::LeaveCriticalSection(&g_cs); } return 0; } //单线程将g_nCount加到5亿 void Add() { g_nCount++; } int _tmain(int argc, _TCHAR* argv[]) { //系统频率计数(秒) LARGE_INTEGER liFre,liStart,liEnd; QueryPerformanceFrequency(&liFre); //返回定时器的频率。 //开始计时 QueryPerformanceCounter(&liStart); //返回定时器当前计数值。 //系统时间计数(毫秒)。ticktime计时开始 //int nStart=GetTickCount(); //初始化临界区 ::InitializeCriticalSection(&g_cs); //启动线程 HANDLE hThread[g_nThreadNum]; for (int i = 0; i < g_nThreadNum; ++i) { hThread[i]= (HANDLE)_beginthreadex(NULL, 0, ThreadFunc, NULL, 0, 0); if (pThreads[i] == 0) { continue; i--; } printf("Thread :%d\n", i); //Add(); } //等待线程结束 WaitForMultipleObjects(g_nThreadNum, hThread, TRUE, INFINITE); //计时结束 QueryPerformanceCounter(&liEnd); LONGLONG llTime = (liEnd.QuadPart - liStart.QuadPart)*1000 / (int)liFre.QuadPart; printf("QueryPerformance: %d\n", llTime); //ticktime计时结束 //int nTime = (GetTickCount() - nStart); //printf("TickTime:%d\n", nTime); printf("g_nCount:%d\n", g_nCount); //释放资源 for (int i = 0; i < g_nThreadNum; ++i) { CloseHandle(hThread[i]); } ::DeleteCriticalSection(&g_cs); getchar(); return 0; }