Windows线程同步(上)
先介绍一个创建线程的API,参考:https://msdn.microsoft.com/en-us/library/windows/desktop/ms682453%28v=vs.85%29.aspx
Creates a thread to execute within the virtual address space of the calling process.
HANDLE WINAPI CreateThread(
_In_opt_ LPSECURITY_ATTRIBUTES lpThreadAttributes,
_In_ SIZE_T dwStackSize,
_In_ LPTHREAD_START_ROUTINE lpStartAddress,
_In_opt_ LPVOID lpParameter,
_In_ DWORD dwCreationFlags,
_Out_opt_ LPDWORD lpThreadId
);
使用范例参考MSDN:https://msdn.microsoft.com/en-us/library/windows/desktop/ms682516%28v=vs.85%29.aspx
举例说明线程:
#include <iostream> #include <windows.h> using namespace std; #define MAX_THREADS 20 #define BUF_SIZE 255 DWORD dwAccessOrder = 0; DWORD WINAPI MyThreadFunction(LPVOID lpPara) { DWORD *dwPara = (DWORD*)lpPara; cout << "这是第" << *dwPara << "个启动的线程!" << endl; return 0; } int main() { DWORD dwThreadIdArray[MAX_THREADS]; HANDLE hThreadArray[MAX_THREADS]; LPVOID lpParam = nullptr; for (int i = 0; i < MAX_THREADS; i++) { lpParam = &i; //可能有如下情况发生:参数还没有传递给线程,就已经把线程起起来了,比如i=12时,起起来了一个线程 //线程还未接收到参数12时,就进入了下一次循环,i被覆盖,被赋值为了13,于是这时候又 //起起来一个线程,可能就和刚刚的那个线程同时接到了参数13 //而后接收到参数的线程,却又有可能比先接收到参数的线程输出到窗口 hThreadArray[i] = CreateThread( NULL, 0, MyThreadFunction, lpParam, 0, &dwThreadIdArray[i]); } cout << "这里是主线程" << endl; for (int i = 0; i < MAX_THREADS; i++) { CloseHandle(hThreadArray[i]); } system("pause"); return 0; }
输出如图,毫无顺序性:
由此我们可以判定,被创建出来的线程以及创建这些线程的主线程是独立的执行体。他们被“迅速”创建之后,就开始“独立行动”,去“争抢”CPU。其实,线程是由操作系统按照一定的调度算法来决定谁先谁后执行。多线程实际上是异步的,它的目的就是快速的、交替的获取CPU的执行权,让用户以为这些线程是并行执行的。
我们对上边的例子稍作修改:
#include <iostream> #include <windows.h> using namespace std; #define MAX_THREADS 50 #define BUF_SIZE 255 DWORD dwAccessTimes = 0; DWORD WINAPI MyThreadFunction(LPVOID lpParam) { Sleep(500); dwAccessTimes++; Sleep(500); return 0; } int main() { DWORD dwThreadIdArray[MAX_THREADS]; HANDLE hThreadArray[MAX_THREADS]; LPVOID lpParam = nullptr; int nLoopTimes = 20; while (nLoopTimes >=0) { dwAccessTimes = 0; for (int i = 0; i < MAX_THREADS; i++) { hThreadArray[i] = CreateThread( NULL, 0, MyThreadFunction, NULL, 0, &dwThreadIdArray[i]); } WaitForMultipleObjects(MAX_THREADS, hThreadArray, TRUE, INFINITE); cout << "访问次数为:" << dwAccessTimes << endl; for (int i = 0; i < MAX_THREADS; i++) { CloseHandle(hThreadArray[i]); } nLoopTimes--; } system("pause"); return 0; }
输出如下结果:
或者
为什么并非每行都是输出50?
我们在dwAccessTimes处下断点,发现:
“dwAccessTimes++”实际上是由三句汇编语句构成的。这样由于线程执行的并发性,很可能线程A执行到第二句时,线程B开始执行,线程B将原来的值又写入寄存器eax中,这样线程A所主要计算的值就被线程B修改了。这样执行下来,结果是不可预知的——可能会出现50,可能小于50。
比如,
mov eax,g_nNum
add eax
mov g_nNum,eax
y线程执行完前两句汇编后,x线程将g_nNum=10读入eax,add eax后eax值为11,此时y线程获取CPU并继续执行,执行mov g_nNum,eax,就把被x篡改了的数据给了g_nNum.
当线程t1和t2都需要访问变量dwAccessTimes时,如果t1正在修改dwAccessTimes的值但是尚未完成,此时操作系统把执行权切换到了t2,此时t2要读取dwAccessTimes就得到了一个错误的值。因此,我们必须使用某种手段使得需要被多个线程共享的数据在同一时刻只能被一个线程访问。这种手段,叫做线程同步。
参考:http://blog.csdn.net/morewindows/article/details/7429155
- 线程同步一:临界区
临界区是线程同步的一种实现方式。Windows提供了4个关于临界区的函数 (InitializeCriticalSection,EnterCriticalSection,LeaveCriticalSection,DeleteCriticalSection), 要想使用这些函数,必须先有一个临界区变量,
CRITICAL_SECTION cs;
临界区变量,不能复制,不能移动,也不能读取里面的字段(可以取临界区变量的地址)。总而言之,我们在编写程序时,必须把临界区当做一个黑箱,一切对临界区的操作必须通过那4个函数来进行。
在使用临界区之前,必须用InitializeCriticalSection函数将其初始化:
InitializeCriticalSection(&cs);
一个线程可以通过EnterCriticalSection函数来进入一个临界区:
EnterCriticalSection(&cs);
一旦一个线程进入了某个临界区,其他线程便不能进入这个临界区。
一个线程可以通过LeaveCriticalSection函数来离开一个临界区:
LeaveCriticalSection(&cs);
当一个线程离开一个临界区之后,其他线程可以进入这个临界区。
当程序不再需要这个临界区时,别忘记将其销毁。
DeleteCriticalSection(&cs);
临界区的用法是,如果一个变量(记为x)需要被多个线程共享时,那么可以搞一个临界区。任何一个线程在访问x之前,必须先进入临界区,完成对x的访问后,离开临界区。因为一个临界区在同一时刻只允许一个线程进入,这样就保证了x在同一时刻只能被一个线程访问。
#include <iostream> #include <windows.h> using namespace std; #define MAX_THREADS 50 #define BUF_SIZE 255 DWORD dwAccessTimes = 0; CRITICAL_SECTION cs; DWORD WINAPI MyThreadFunction(LPVOID lpParam) { Sleep(500); EnterCriticalSection(&cs); dwAccessTimes++; LeaveCriticalSection(&cs); Sleep(500); return 0; } int main() { DWORD dwThreadIdArray[MAX_THREADS]; HANDLE hThreadArray[MAX_THREADS]; LPVOID lpParam = nullptr; int nLoopTimes = 20; InitializeCriticalSection(&cs); while (nLoopTimes>=0) { dwAccessTimes = 0; for (int i = 0; i < MAX_THREADS; i++) { hThreadArray[i] = CreateThread( NULL, 0, MyThreadFunction, NULL, 0, &dwThreadIdArray[i]); } WaitForMultipleObjects(MAX_THREADS, hThreadArray, TRUE, INFINITE); cout << "访问次数为:" << dwAccessTimes << endl; for (int i = 0; i < MAX_THREADS; i++) { CloseHandle(hThreadArray[i]); } nLoopTimes--; } DeleteCriticalSection(&cs); system("pause"); return 0; }
输出结果:
再来看:
#include <iostream> #include <windows.h> using namespace std; #define MAX_THREADS 20 CRITICAL_SECTION g_csThreadParameter, g_csThreadCode; int g_nNum = 0; DWORD WINAPI MyThreadFunction(LPVOID lpParam) { int nThreadNum = *(int *)lpParam; LeaveCriticalSection(&g_csThreadParameter);//离开子线程序号关键区域 Sleep(500); EnterCriticalSection(&g_csThreadCode); g_nNum++; Sleep(500);//some work should to do cout << "参数 " << nThreadNum << " : " << "资源:" << g_nNum << endl; LeaveCriticalSection(&g_csThreadCode); return 0; } int main() { HANDLE hThreadArray[MAX_THREADS]; DWORD dwThreadIdArray[MAX_THREADS] = { 0 }; //关键段初始化 InitializeCriticalSection(&g_csThreadParameter); InitializeCriticalSection(&g_csThreadCode); g_nNum = 0; int i = 0; while (i < MAX_THREADS) { EnterCriticalSection(&g_csThreadParameter);//进入子线程序号关键区域 hThreadArray[i] = CreateThread( NULL, 0, MyThreadFunction, &i, 0, &dwThreadIdArray[i]); ++i; } WaitForMultipleObjects(MAX_THREADS, hThreadArray, TRUE, INFINITE); DeleteCriticalSection(&g_csThreadCode); DeleteCriticalSection(&g_csThreadParameter); system("pause"); return 0; }
这里可以看到,资源是按序输出的,因为while循环中按顺序启动线程,他们会互斥的去访问并修改g_nNum,并不会出现在x线程修改g_nNum的时候,y也去操作g_nNum的情况。这里要说明,这里输出“资源:1”的并不一定就是第一个启动的线程。这里只是实现了资源的互斥访问,并没有给线程“排序”。
但是“参数”的输出有些异样,说明主线程与子线程之间的同步有问题。我们在EnterCriticalSection和LeaveCriticalSection两处下断点:
理想情况下,我们期待的互斥效果是在EnterCriticalSection和LeaveCriticalSection两个断点出轮流断下的,而事实上却是在EnterCriticalSection断下好几次之后才在LeaveCriticalSection处断下一次。这说明,主线程执行EnterCriticalSection、创建线程,但是线程还没来得及去执行自己内部的代码,主线程又去执行了“EnterCriticalSection、创建线程”这两步。抢在子线程之前,主线程又获得了执行权。这里有一个“所有权”的概念,当EnterCriticalSection后,主线程对这个临界区具有所有权,且所有者可以反复进入,并用RecursionCount记录进入次数,如主线程对:
具有了所有权,那么它可以反复进入,在:
之前,不断的更新:
假设此时,nThreadNum被更新为13,那么当它LeaveCriticalSection后,可能连续多个子线程获得的nThreadNum的值就都是13了。
在g_nNum,前后加上EnterCriticalSection和LeaveCriticalSection是为了让诸多线程互斥访问它,在CreateThread加上EnterCriticalSection和LeaveCriticalSection的目的是想要互斥的去创建线程,创建一个线程、执行这个线程,然后再返回主线程,再去创建线程,再去执行这个线程,依次循环。然而现实并非如此,上边有了解释,下边再细致说说:
与诸多线程访问g_nNum不同,CreateThread只有主线程去“访问”。这里我们不得不去理解一下结构CRITICAL_SECTION:
该结构解释如下,也可参考:http://www.cnblogs.com/dirichlet/archive/2011/03/16/1986251.html《深入理解CRITICAL_SECTION》
DebugInfo 此字段包含一个指针,指向系统分配的伴随结构,该结构的类型为 RTL_CRITICAL_SECTION_DEBUG。这一结构中包含更多极有价值的信息,也定义于 WINNT.H 中。我们稍后将对其进行更深入地研究。
LockCount 这是临界区中最重要的一个字段。它被初始化为数值 -1;此数值等于或大于 0 时,表示此临界区被占用。当其不等于 -1 时,OwningThread 字段(此字段被错误地定义于 WINNT.H 中 — 应当是 DWORD 而不是
HANDLE)包含了拥有此临界区的线程 ID。此字段与 (RecursionCount -1) 数值之间的差值表示有多少个其他线程在等待获得该临界区。
RecursionCount
此字段包含所有者线程已经获得该临界区的次数。如果该数值为零,下一个尝试获取该临界区的线程将会成功。
OwningThread 此字段包含当前占用此临界区的线程的线程标识符。此线程 ID 与 GetCurrentThreadId 之类的 API 所返回的 ID 相同。
LockSemaphore
此字段的命名不恰当,它实际上是一个自复位事件,而不是一个信号。它是一个内核对象句柄,用于通知操作系统:该临界区现在空闲。操作系统在一个线程第一次尝试获得该临界区,但被另一个已经拥有该临界区的线程所阻止时,自动创建这样一个句柄。应当调用 DeleteCriticalSection(它将发出一个调用该事件的 CloseHandle 调用,并在必要时释放该调试结构),否则将会发生资源泄漏。
SpinCount 仅用于多处理器系统。MSDN 文档对此字段进行如下说明:“在多处理器系统中,如果该临界区不可用,调用线程将在对与该临界区相关的信号执行等待操作之前,旋转 dwSpinCount
次。如果该临界区在旋转操作期间变为可用,该调用线程就避免了等待操作。”旋转计数可以在多处理器计算机上提供更佳性能,其原因在于在一个循环中旋转通常要快于进入内核模式等待状态。此字段默认值为零,但可以用
InitializeCriticalSectionAndSpinCount API 将其设置为一个不同值。
这个结构中的OwningThread记录拥有这个临界区的线程句柄(即这里有一个“所有权”的概念),如果这个线程再次进入,EnterCriticalSection会更新参数RecursionCount以记录该线程进入的次数并立即返回让该线程进入。其它线程调用EnterCriticalSection企图进入这个临界区时就会被切换到等待状态,一旦拥有这个临界区的线程调用LeaveCriticalSection并使其进入次数减少至0时,系统会自动更新临界区数据并将等待中的线程切换到可调度状态。这个例子中,由于主线程拥有这个临界区资源,所以它可以重复进入临界区从而导致子线程在接收到参数之前,主线程就已经修改了这个参数。所以CRITICAL_SECTION可以用于线程间的互斥,而不可以用于同步。
参考:http://blog.csdn.net/morewindows/article/details/7442639
http://my.oschina.net/jthmath/blog/400510
- 线程同步二:互斥
创建:创建互斥锁的方法是调用函数CreateMutex:
HANDLE WINAPI CreateMutex(
_In_opt_ LPSECURITY_ATTRIBUTES lpMutexAttributes,
_In_ BOOL bInitialOwner,
_In_opt_ LPCTSTR lpName
);
第一个参数是一个指向SECURITY_ATTRIBUTES结构体的指针,一般的情况下,可以是NULL。
第二个参数类型为BOOL,表示互斥锁创建出来后是否被当前线程持有。
第三个参数类型为字符串(const TCHAR*),是这个互斥锁的名字,如果是NULL,则互斥锁是匿名的。
打开:打开一个互斥量。
HANDLE WINAPI OpenMutex(
_In_ DWORD dwDesiredAccess,
_In_ BOOL bInheritHandle,
_In_ LPCTSTR lpName
);
等待:WaitForSingleObject.它的作用是等待,直到一定时间之后,或者,其他线程均不持有hMutex。第二个参数是等待的时间(单位:毫秒),如果该参数为INFINITE,则该函数会一直等待下去。
DWORD WINAPI WaitForSingleObject(
_In_ HANDLE hHandle,
_In_ DWORD dwMilliseconds
);
释放:用ReleaseMutex函数可以让当前线程“放开”一个互斥锁(不持有它了),以便让其他线程可以持有它。
BOOL WINAPI ReleaseMutex(
_In_ HANDLE hMutex
);
销毁:当程序不再需要互斥锁时,要销毁它。
CloseHandle(hMutex);
命名互斥锁:如果CreateMutex函数的第三个参数传入一个字符串,那么所创建的锁就是命名的。当一个命名的锁被创建出来以后,当前进程和其他进程如果试图创建相同名字的锁,CreateMutex会返回原来那把锁的句柄,并且GetLastError函数会返回ERROR_ALREADY_EXISTS。这个特点可以使一个程序在同一时刻最多运行一个实例。
我们模仿上一段程序,尝试使用互斥量实现主线程和子线程之间的同步:
#include <iostream> #include <windows.h> using namespace std; long g_nNum; #define MAX_THREADS 20 HANDLE g_hThreadParameter; CRITICAL_SECTION g_csThreadCode; DWORD WINAPI MyThreadFunction(LPVOID lpParam) { //WaitForSingleObject(g_hThreadParameter, INFINITE);用来获取互斥量。如果不在WaitForSingleObject之前ReleaseMutex,就是白白等待。 //必须先释放互斥量,才能获取,所以此处若加上WaitForSingleObject就会陷入一直等待的状态。 int nThreadNum = *(int *)lpParam; ReleaseMutex(g_hThreadParameter);//释放互斥量以便供其他线程获取 Sleep(500); EnterCriticalSection(&g_csThreadCode); g_nNum++; Sleep(500);//some work should to do cout << "参数 " << nThreadNum << " : " << "资源:" << g_nNum << endl; LeaveCriticalSection(&g_csThreadCode); return 0; } int main() { //初始化互斥量与关键段 第二个参数为TRUE表示互斥量为创建线程所有 g_hThreadParameter = CreateMutex(NULL, FALSE, NULL); InitializeCriticalSection(&g_csThreadCode); HANDLE hThreadArray[MAX_THREADS]; DWORD dwThreadIdArray[MAX_THREADS] = { 0 }; g_nNum = 0; int i = 0; while (i < MAX_THREADS) { WaitForSingleObject(g_hThreadParameter, INFINITE);//等待获取互斥量 hThreadArray[i] = (HANDLE)CreateThread(NULL, 0, MyThreadFunction, &i, 0, &dwThreadIdArray[i]); //WaitForSingleObject(g_hThreadParameter, INFINITE); //等待互斥量被触发 i++; } WaitForMultipleObjects(MAX_THREADS, hThreadArray, TRUE, INFINITE); //销毁互斥量和关键段 CloseHandle(g_hThreadParameter); DeleteCriticalSection(&g_csThreadCode); for (i = 0; i < MAX_THREADS; i++) CloseHandle(hThreadArray[i]); system("pause"); return 0; }
我们用上一例子的思路和方法,同样可以得出:互斥量无法解决线程间同步的问题。
另外,由于互斥量常用于多进程之间的线程互斥,所以它比临界区还多一个特性——“遗弃”情况的处理。比如一个占有互斥量的线程在调用ReleaseMutex()释放互斥量之前就以外终止了(被遗弃了),那么所有等待这个互斥量的线程是否会由于该互斥量无法被获得而陷入一个无穷等待过程中?答案是否定的。因为线程被终止说明它不再使用被互斥量保护的资源,所以这些资源完全应当被其它线程使用。出现这种情况,系统自动把该互斥量内部的线程ID设为0,并将它的递归计数器设为0,以表示这个互斥量被激活了。
进程间互斥实验:
testProMutex1:
#include <iostream> #include <windows.h> using namespace std; const char MUTEX_NAME[] = "{A600A4AA-A8D7-4715-A43F-9BABE97BBCE9}"; int main() { HANDLE hMutex = CreateMutex(NULL, TRUE, MUTEX_NAME); //创建互斥量 cout << "互斥量已经创建,现在按任意键释放互斥量" << endl; getchar(); ReleaseMutex(hMutex); cout << "互斥量已经释放" << endl; CloseHandle(hMutex); system("pause"); return 0; }
testProMutex2:
#include <iostream> #include <windows.h> using namespace std; const char MUTEX_NAME[] = "{A600A4AA-A8D7-4715-A43F-9BABE97BBCE9}"; int main() { HANDLE hMutex = OpenMutex(MUTEX_ALL_ACCESS, TRUE, MUTEX_NAME); //打开互斥量 if (hMutex == NULL) { cout << "打开互斥量失败" << endl; return 0; } cout << "等待获取互斥量......" << endl; DWORD dwResult = WaitForSingleObject(hMutex, 200 * 1000); //等待互斥量被触发 switch (dwResult) { case WAIT_ABANDONED: cout << "拥有互斥量的进程意外终止" << endl; break; case WAIT_OBJECT_0: cout << "已经收到信号" << endl; break; case WAIT_TIMEOUT: cout << "信号未在规定的时间内送到" << endl; break; } CloseHandle(hMutex); system("pause"); return 0; }
先运行testProMutex1,再运行testProMutex2:
1释放互斥量,2获取互斥量:
参考:http://blog.csdn.net/morewindows/article/details/7470936
http://my.oschina.net/jthmath/blog/400660
- CRITICAL_SECTION和Mutex的区别:
- 参考:
http://blog.csdn.net/dazhong159/article/details/7927034
http://www.cnblogs.com/staring-hxs/p/3664765.html《window下线程同步之(Mutex(互斥器) )》