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线程同步详解的主要内容,如果未能解决你的问题,请参考以下文章

Windows线程同步详解

线程同步(windows平台):临界区

Java多线程:线程同步详解

多线程 Thread 线程同步 synchronized

20160226.CCPP体系详解(0036天)

20160226.CCPP体系详解(0036天)