Windows线程同步详解
Posted
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Windows线程同步详解相关的知识,希望对你有一定的参考价值。
线程同步问题
在多线程编程中,极容易产生错误。造成错误的原因:两个或多个线程同时访问了共有的资源(比如全局变量,句柄,对空间等),造成资源在不同线程修改时出现不一致。多个线程对于资源的访问要按照一定的先后顺序,但是未按照预想的顺序来,就会导致程序出现意想不到的错误。
问题实例:(环境:vs2015 控制台程序)#include<Windows.h> #include<stdio.h> int g_nNum = 0; DWORD WINAPI ThreadProc(LPVOID lParam) { for (int i = 0; i < 10000; i++) { g_nNum++; } printf("%d", g_nNum); return 0; } int main() { //创建线程1 HANDLE hThread1 = CreateThread(NULL, NULL, ThreadProc, NULL, NULL, NULL); //创建线程2 HANDLE HThread2 = CreateThread(NULL, NULL, ThreadProc, NULL, NULL, NULL); WaitForSingleObject(hThread1, INFINITE); //线程1执行完毕后返回 WaitForSingleObject(HThread2, INFINITE); //线程2执行完毕后返回 printf("%d\n", g_nNum); return 0; }
第一次执行结果:
11789
17876
17876
第二次执行结果:
20000
15844
20000
按照预期,g_nNum在两个线程中应该各自自增10000,而实际上,g_nNum的值确是不确定的。
首先来看一下自增这个简单的操纵在汇编层的代码:00AE1419 mov eax,dword ptr ds [00AE8134h] 00AE141E add eax,1 00AE1421 mov dword ptr ds:[00AE8134h],eax
两个线程同时执行g_nNum++这个操作,有可能线程1执行了add eax,还没有将将自增的结果写入,线程2又开始执行,当线程1再执行的时候,线程2的执行就相当于已经无用。因为线程的调度是不可控的,所以我们不能预知最后的结果。
解决方案:**
1.原子操作
原子操作是一些比较简单的操作,只能对资源进行简单的加减赋值等。当运用原子操作访问某数据时,其他线程不能在此次操作结束前访问此数据,即不允许两个线程同时操作一个数据,当然,也不允许三个。原子操作就像厕所,只允许一个人进入。
常见的原子操作函数自行百度int g_nNum = 0; DWORD WINAPI ThreadProc(LPVOID lParam) { for (int i = 0; i < 10000; i++) { //原子操作中的自增,其他的原子操作函数自行百度 InterlockedIncrement((unsigned long*)&g_nNum); } printf("%d", g_nNum); return 0; } int main() { HANDLE hThread1 = CreateThread(NULL, NULL, ThreadProc, NULL, NULL, NULL); HANDLE HThread2 = CreateThread(NULL, NULL, ThreadProc, NULL, NULL, NULL); WaitForSingleObject(hThread1, INFINITE); WaitForSingleObject(HThread2, INFINITE); printf("%d\n", g_nNum); return 0; }
运行结果
10000
20000
20000
2.临界区
原子操作仅能够解决单独的数据(整型变量的基本运算)的线程同步问题,大多数时候,我们想要实现的是对一个代码段的保护,于是便引入了临界区这一概念。临界区通过EnterCriticalSection与LeaveCriticalSection这一对函数,通过这个函数对,就可以实现多个代码保护区。在使用临界区前,需要调用InitiaizeCriticalSection初始化一个临界区,使用完后调用DeleteCriticalSection销毁临界区。
#include <windows.h>
CRITICAL_SECTION cs = {};
int g_nNum = 0;
DWORD WINAPI ThreadProc(LPVOID lParam) {
// 2. 进入临界区
// cs有个属性LockSemaphore是不是被锁定
// 当调用EnterCriticalSection表示临界区被锁定,OwningThread就是该线程
// 其他调用EnterCriticalSection,会检查和锁定时的线程是否是同一个线程
// 如果不是,调用Enter的线程就阻塞
// 如果是,就把锁定计数LockCount+1
// 有几次Enter就得有几次Leave
// 但是,不是拥有者线程的人不能主动Leave
EnterCriticalSection(&cs);
for (int i = 0; i < 100000; i++)
{
g_nNum++;
}
printf("%d\n", g_nNum);
// 3. 离开临界区
// 万一,还没有调用Leave,该线程就崩溃了,或死循环了..
// 外面等待的人就永远等待
// 临界区不是内核对象, 不能跨进程同步
LeaveCriticalSection(&cs);
return 0;
}
int main()
{
// 1. 初始化临界区
InitializeCriticalSection(&cs);
HANDLE hThread1 = CreateThread(NULL, NULL, ThreadProc, NULL, NULL, NULL);
HANDLE hThread2 = CreateThread(NULL, NULL, ThreadProc, NULL, NULL, NULL);
WaitForSingleObject(hThread1, INFINITE);
WaitForSingleObject(hThread2, INFINITE);
printf("%d\n", g_nNum);
// 4. 销毁临界区
DeleteCriticalSection(&cs);
return 0;
}
3.互斥体
临界区有很多解决不了的问题,因为临界区在一个进程中有效,无法在多进程的情况下进行同步。并且,如果一个线程进入到临界区,结果这个线程由于某些原因奔溃了,即无法执LeaveCriticalSection(),那么其他线程将无法再进入临界区,程序奔溃。而互斥体则可以解决这些问题。
首先,互斥体是一个内核对象。(因此互斥体拥有内核对象的一切属性)它有两个状态,激发态和非激发态;它有一个概念叫做线程拥有权,与临界区类似;等待函数等待互斥体的副作用,将互斥体的拥有者设置为本线程,然后将互斥体的状态设置为非激发态。
主要函数:CreateMutex();WaitForSingleObject();ReleaseMutex();函数用法自行百度。
当一个线程A调用WaitForSingleObject函数时,WaitForSingleObject会立即返回,将并将互斥体设为非激发态,互斥体被锁住,此线程获得拥有权。之后,任何调用WaitForSingleObject的线程无法获得所有权,必须等待互斥体。当线程A调用ReleaseMutex时,互斥体被解锁,此时互斥体又被设置为激发态,并会从等待它的线程中随机选一个,重复前面的过程。互斥体一次只能被一个线程拥有,在WaitXXXX与ReleaseMutex之间的代码被保护起来,这一点与临界区类似,只不过互斥体是一个内核对象,可以进行多进程同步。
#include <windows.h>
#include<stdio.h>
HANDLE hMutex = 0;
int g_nNum = 0;
// 临界区和互斥体比较
// 1. 互斥体是个内核对象,可以跨进程同步,临界区不行
// 2. 当他们的拥有者线程都崩溃的时候,互斥体可以被系统释放,变为有信号,其他的等待函数可以正常返回
// 临界区不行,如果都是假死(死循环,无响应),他们都会死锁
// 3. 临界区不是内核对象,所以访问速度比互斥体快
DWORD WINAPI ThreadProc(LPVOID lParam) {
// 等待某个内核对象,有信号就返回,无信号就一直等待
// 返回时把等待的对象变为无信号状态
WaitForSingleObject(hMutex, INFINITE);
for (int i = 0; i < 100000; i++)
{
g_nNum++;
}
printf("%d\n", g_nNum);
// 把互斥体变为有信号状态
ReleaseMutex(hMutex);
return 0;
}
int main()
{
// 1. 创建一个互斥体
hMutex = CreateMutex(
NULL,
FALSE,// 是否创建时就被当先线程拥有
NULL);// 互斥体名称
HANDLE hThread1 = CreateThread(NULL, NULL, ThreadProc, NULL, NULL, NULL);
HANDLE hThread2 = CreateThread(NULL, NULL, ThreadProc, NULL, NULL, NULL);
WaitForSingleObject(hThread1, INFINITE);
WaitForSingleObject(hThread2, INFINITE);
printf("%d\n", g_nNum);
return 0;
}
4.信号量
信号量与互斥体类似。不过信号量中引入了信号数量的概念。如果说互斥体是家里厕所,在一个时间点只能一个人使用,那信号量就是公共厕所,可以多个人同时使用,但是仍有上限。这个上限数量即最大信号数量。
主要函数:CreateSemaphore();OpenSemaphore();ReleaseSemaphore();WaitForSingleObject(); 函数用法自行百度
当有线程调用了WaitForSingleObject();当前信号量减一,再有线程调用,再减一。为0时,即信号量被锁住,再有线程调用WaitForSingleObject时,将被阻塞。
#include <windows.h>
#include <stdio.h>
HANDLE hSemphore;
int g_nNum = 0;
DWORD WINAPI ThreadProc(LPVOID lParam) {
WaitForSingleObject(hSemphore, INFINITE);
for (int i = 0; i < 100000; i++)
{
g_nNum++;
}
printf("%d\n", g_nNum);
ReleaseSemaphore(hSemphore,
1,// 释放的信号个数可以大于1,但是释放后的信号个数+之前的不能大于最大值,否则释放失败
NULL);
return 0;
}
int main()
{
hSemphore = CreateSemaphore(
NULL,
1,// 初始信号个数
1,// 最大信号个数,就是允许同时访问保护资源的线程数
NULL);
HANDLE hThread1 = CreateThread(NULL, NULL, ThreadProc, NULL, NULL, NULL);
HANDLE hThread2 = CreateThread(NULL, NULL, ThreadProc, NULL, NULL, NULL);
WaitForSingleObject(hThread1, INFINITE);
WaitForSingleObject(hThread2, INFINITE);
printf("%d\n", g_nNum);
return 0;
}
5.事件
事件具有较大的权限。可以手动设置事件对象为激发态还是非激发态。创建时间对象的时候,可以设置是自动选择和手动选择。自动选择的事件,等待函数返回时,会自动将其状态设置为非激发态,阻塞其他线程。手动选择的,事件对象状态的控制全靠代码。
主要函数:CreateEventW();OpenEventA();SetEvent();PulseEvent();
CloseEvent();RoseEvent();
#include <windows.h>
#include<stdio.h>
HANDLE hEvent1, hEvent2;
DWORD WINAPI ThreadProcA(LPVOID lParam) {
for (int i = 0; i < 10; i++){
WaitForSingleObject(hEvent1, INFINITE);
printf("A ");
SetEvent(hEvent2);
}
return 0;
}
DWORD WINAPI ThreadProcB(LPVOID lParam) {
for (int i = 0; i < 10; i++){
WaitForSingleObject(hEvent2, INFINITE);
printf("B ");
SetEvent(hEvent1);
}
return 0;
}
int main()
{
// 事件对象,高度自定义的
hEvent1 = CreateEvent(
NULL,
FALSE,// 自动重置
TRUE,// 有信号
NULL);
// hEvent1自动重置 初始有信号 任何人通过setevent变为有信号 resetevent变为无信号
// hEvent2自动重置 初始无信号
hEvent2 = CreateEvent(NULL, FALSE, FALSE, NULL);
HANDLE hThread1 = CreateThread(NULL, NULL, ThreadProcA, NULL, NULL, NULL);
HANDLE hThread2 = CreateThread(NULL, NULL, ThreadProcB, NULL, NULL, NULL);
WaitForSingleObject(hThread1, INFINITE);
WaitForSingleObject(hThread2, INFINITE);
return 0;
}
以上是关于Windows线程同步详解的主要内容,如果未能解决你的问题,请参考以下文章