线程同步问题

本文详细介绍了线程同步和互斥的概念,通过实例代码展示了线程同步中线程的先后执行顺序以及互斥中对资源的独占访问。文章探讨了原子操作、临界区、互斥体、事件和信号量等解决线程同步和互斥问题的机制,并通过具体示例解释了各自的工作原理和应用场景。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

什么是线程同步和互斥???

如果你编写的是多线程的程序,那么多个线程的并发执行,可以认为他们是同时执行代码的,但是线程和线程之间并非是毫无关系的,很多时候会有以下的两种关系:

a) 线程A的继续执行 要以线程B完成了某一个操作后为前提,这种称为线程同步

b) 多个线程不可同时修改一个资源(全局变量 ,数据结构,对象),这种称为线程的互斥

为什么需要线程同步和互斥

线程同步:

// 09_线程同步的问题.cpp : 此文件包含 "main" 函数。程序执行将在此处开始并结束。
//

#include <iostream>
#include <Windows.h>
int g_n;
DWORD WINAPI ThreadPro1(LPVOID lpThreadParameter) {
    printf("打开文件\r\n");
    return 0;
}
DWORD WINAPI ThreadPro2(LPVOID lpThreadParameter) {
    printf("读写文件\r\n");
    return 0;
}

int main (){
    HANDLE hThread1 = 0, hThread2;
    hThread1 = CreateThread(NULL, NULL, ThreadPro1, NULL, NULL, NULL);
    hThread2 = CreateThread(NULL, NULL, ThreadPro2, NULL, NULL, NULL);
    WaitForSingleObject(hThread1, -1);
    WaitForSingleObject(hThread2, -1);
    //printf("%d", g_n);
    return 0;
}

在我们写代码的时候都知道 代码的执行是要有先后顺序的 比如:我们读写文件 要先打开文件才能读写 读写的动作是建立在打开文件的基础上 线程也是如此 这里只是简单的举个例子 只有两个线程 代码也不复杂 当线程多了 代码变得复杂 结果就不是我们预期的结果了

线程互斥:

两个线程对同一个变量进行了操作,结果是随机的

// 09_线程同步的问题.cpp : 此文件包含 "main" 函数。程序执行将在此处开始并结束。
//

#include <iostream>
#include <Windows.h>
int g_n;
DWORD WINAPI ThreadPro1(LPVOID lpThreadParameter) {
    for (int i = 0; i < 100000; i++)
        g_n++;
    return 0;
}
DWORD WINAPI ThreadPro2(LPVOID lpThreadParameter) {
    for (int i = 0; i < 100000; i++)
        g_n++;
    return 0;
}
int main (){
    HANDLE hThread1 = 0, hThread2;
    hThread1 = CreateThread(NULL, NULL, ThreadPro1, NULL, NULL, NULL);
    hThread2 = CreateThread(NULL, NULL, ThreadPro2, NULL, NULL, NULL);
    printf("%d", g_n);
    return 0;
}

我们看最后的结果并非是正确的结果 这就是线程互斥问题 接下来有几个机制完美的解决了互斥和同步的问题

原子操作

特性:解决互斥问题  对于一个变量的基本算数运算保证是原子性的,换句话说就是我操作变量的时候别人是不能操作的

函数作用
InterlockedIncrement自增
InterlockedDecrement自减
InterlockedExchange赋值

 保证同一时间只有一个线程操作

/*
* 原子操作
两个进程访问同一个全局变量
*/
#include <iostream>
#include <Windows.h>
long g_n;
DWORD WINAPI ThreadPro1(LPVOID lpThreadParameter) {
    for (int i = 0; i < 100000; i++)
        InterlockedIncrement(&g_n);
    //g_n++;
    return 0;
}
DWORD WINAPI ThreadPro2(LPVOID lpThreadParameter) {
    for (int i = 0; i < 100000; i++)
        InterlockedIncrement(&g_n);
    //g_n++;
    return 0;
}
int main() {
    HANDLE hThread1 = 0, hThread2;
    hThread1 = CreateThread(NULL, NULL, ThreadPro1, NULL, NULL, NULL);
    hThread2 = CreateThread(NULL, NULL, ThreadPro2, NULL, NULL, NULL);
    WaitForSingleObject(hThread1, -1);
    WaitForSingleObject(hThread2, -1);
    printf("%d", g_n);
    return 0;
}

结果是正确的

 缺点:只能保证在操作一个变量的时候是互斥的 我们通常是要保证一段代码,这个时候原子操作就满足不了我们的需求了

临界区

特性:解决互斥问题 不可跨进程 有线程所有权概念 不是内核对象,能保证一段代码在同一时间只有一个线程操作

线程所有权:这个线程加锁就要这个线程解锁 

函数作用备注
InitializeCriticalSection初始化临界区
EnterCriticalSection进入临界区
LeaveCriticalSection离开临界区
DeleteCriticalSection销毁
/*
临界区
保证一段代码不能被两个进程同时执行
初始化临界区以后 在要保护的代码前后加上锁和开锁
引用计数概念 上几次锁就要开几次 
自己上锁自己开 
*/
#include <iostream>
#include <Windows.h>
long g_n;
CRITICAL_SECTION g_cs = { 0 };
DWORD WINAPI ThreadPro1(LPVOID lpThreadParameter)
{
    for (int i = 0; i < 100000; i++)
    {
        //进入临界区,上锁
        EnterCriticalSection(&g_cs);
        g_n++;
        //离开临界区,解锁
        LeaveCriticalSection(&g_cs);

    }
    return 0;
}
DWORD WINAPI ThreadPro2(LPVOID lpThreadParameter)
{
    for (int i = 0; i < 100000; i++)
    {
        //进入临界区,上锁  
        EnterCriticalSection(&g_cs);
        g_n++;
        //离开临界区,解锁
        LeaveCriticalSection(&g_cs);
    }
    return 0;
}
int main() 
{

    //初始化临界区
    InitializeCriticalSection(&g_cs);

    HANDLE hThread1 = 0, hThread2;
    hThread1 = CreateThread(NULL, NULL, ThreadPro1, NULL, NULL, NULL);
    hThread2 = CreateThread(NULL, NULL, ThreadPro2, NULL, NULL, NULL);
    printf("%d", g_n);
    DeleteCriticalSection(&g_cs);
    return 0;
}

缺点:

如果你进入临界区 离开没有开锁 其他的线程是无法进入临界区的 这个时候就会出现一个情况 也就是如果我进入临界区以后 代码有问题 还没有执行解锁就崩了 那我别的程序就进不去了 就会出现死锁的情况

激发态非激发态

再说互斥体之前要了解一个函数和两个概念

激发态:就是有信号 也就是线程可执行

非激发态:没有信号,线程不可执行

WaitForSingleObject函数

WaitForSingleObject(内核对象,时间)

顾名思义等一个对象

作用:

当内核对象处于非激发态的时候,就阻塞住,内核对象处于激发态了 就直接返回    

副作用:

等待函数对于被等待的对象有一些影响,这种影响叫等待函数的副作用.

返回值:

WAIT_ABANDONED

0x00000080L

互斥体的情况下有用

WAIT_OBJECT_0

0x00000000L

等到了内核对象被设置成了激发态

WAIT_TIMEOUT

0x00000102L

超时了

WAIT_FAILED

(DWORD)0xFFFFFFFF

失败了

互斥体

特性:可跨进程,拥有线程所有权,如果有线程崩溃了 互斥体立即变为激发态 等待互斥体的线程会立即获得这个互斥体 互斥体不会造成死锁问题
 

函数作用备注
CreateMutex创建互斥体

可以为互斥体起名字

OpenMutex打开互斥体 得到句柄根据名字才能打开互斥体
ReleaseMutex释放互斥体使互斥体变为激发态
CloseHandle关闭句柄使用完关闭句柄
WaitForSingleObject等待互斥体变为激发态等到激发态后,会使得互斥体再次处于非激发态

#include <iostream>

#include <iostream>
#include <Windows.h>
int g_n;
HANDLE g_hMutex = NULL;
DWORD WINAPI ThreadPro1(LPVOID lpThreadParameter) {
    for (int i = 0; i < 100000; i++)
    {
        WaitForSingleObject(g_hMutex, -1);
        g_n++;
        ReleaseMutex(g_hMutex);
    }
    return 0;
}
DWORD WINAPI ThreadPro2(LPVOID lpThreadParameter) {
    for (int i = 0; i < 100000; i++)
    {
        //一个激发态互斥体变为非激发态 非激发态别的进程不可执行
        WaitForSingleObject(g_hMutex, -1);
        g_n++;
        //使互斥体变为激发态 激发态别的线程才能继续执行
        ReleaseMutex(g_hMutex);
    }
    return 0;
}
int main() {
    HANDLE hThread1 = 0, hThread2;
    g_hMutex = CreateMutex(
        NULL, //安全描述属性,说明这是一个内核对象
        FALSE,//TRUE:初始拥有者是创建互斥体的线程,互斥体为非激发态
              //FALSE:没有拥有者,互斥体为激发态
        NULL  //互斥体的名字 别人可以通过这个名字打开这个互斥体 其他进程
    );
    hThread1 = CreateThread(NULL, NULL, ThreadPro1, NULL, NULL, NULL);
    hThread2 = CreateThread(NULL, NULL, ThreadPro2, NULL, NULL, NULL);
    //等待线程从非激发态变为激发态
    WaitForSingleObject(hThread1, -1);
    WaitForSingleObject(hThread2, -1);
    printf("%d", g_n);
    return 0;
}

事件解决互斥问题 

特性:没有线程所有权概念 任何线程都可以释放事件 可跨进程 解决互斥和互斥体用法有些类似

函数作用备注

CreateEvent

创建事件

可以给事件起名字

可设置两种模式:手工和自动

OpenEvent打开事件,得到句柄根据名字打开事件
setEvent释放事件使事件处于激发态
ResetEvent重置事件会使事件处于非激发态,对手工模式事件有效
WaitForSingleObject等待事件处于激发态等到激发态后,对自动模式得事件会使其在次处于非激发态

事件有两种模式 手动和自动  自动的时候waitforsingnalObject会有副作用 和互斥体很像 也可以当作互斥体来用 手工状态很少用

// 09_线程同步的问题.cpp : 此文件包含 "main" 函数。程序执行将在此处开始并结束。
//

#include <iostream>
#include <Windows.h>
int g_n;
HANDLE g_hEvent = NULL;
DWORD WINAPI ThreadPro1(LPVOID lpThreadParameter) {
    for (int i = 0; i < 100000; i++)
    {
        WaitForSingleObject(g_hEvent, -1);
        g_n++;
        //变为激发态,解锁
        SetEvent(g_hEvent);
    }

    return 0;
}
DWORD WINAPI ThreadPro2(LPVOID lpThreadParameter) {
    for (int i = 0; i < 100000; i++)
    {
        WaitForSingleObject(g_hEvent, -1);
        g_n++;
        //变为激发态,解锁
        SetEvent(g_hEvent);
    }
    return 0;
}
int main() {
    HANDLE hThread1 = 0, hThread2;
    g_hEvent = CreateEvent(
        NULL,  //安全属性
        FALSE, //TRUE:手工设置的事件对象
               //FALSE:自动设置的事件对象
        TRUE,  //TRUE:初始状态是激发态
               //FALSE:初始状态就是非激发态
        NULL
    );
    hThread1 = CreateThread(NULL, NULL, ThreadPro1, NULL, NULL, NULL);
    hThread2 = CreateThread(NULL, NULL, ThreadPro2, NULL, NULL, NULL);
    WaitForSingleObject(hThread1, -1);
    WaitForSingleObject(hThread2, -1);
    printf("%d", g_n);
    return 0;
}

事件解决同步问题 

正因为事件没有线程所有权 任何线程都可以释放事件 所以可以解决线程同步问题

我们之前提过 线程同步也就是说 在想执行线程A 就要先执行线程B,那事件是怎么同步线程的哪

// 14_事件对象解决同步问题.cpp : 此文件包含 "main" 函数。程序执行将在此处开始并结束。
//

#include <iostream>
#include<Windows.h> 


HANDLE g_CreateEvent = NULL;

HANDLE g_OperationEvent = NULL;

DWORD WINAPI CreateFileProc(LPVOID lpThreadParameter)
{
    printf("打开文件\r\n");

    printf("申请缓冲区\r\n");

    printf("关闭文件\r\n");

    return 0;
}

DWORD WINAPI OperationFileProc(LPVOID lpThreadParameter)
{

    printf("获取文件大小\r\n");


    printf("读写文件\r\n");

    return 0;
}
int main()
{
    HANDLE  hWife = NULL;
    HANDLE hHusband = NULL;
    g_CreateEvent = CreateEvent(
        NULL,  //安全属性
        FALSE, //TRUE:手工设置的事件对象
               //FALSE:自动设置的事件对象
        FALSE,  //TRUE:初始状态是激发态
               //FALSE:初始状态就是非激发态
        NULL
    );

    g_OperationEvent = CreateEvent(
        NULL,  //安全属性
        FALSE, //TRUE:手工设置的事件对象
               //FALSE:自动设置的事件对象
        FALSE,  //TRUE:初始状态是激发态
               //FALSE:初始状态就是非激发态
        NULL
    );



    hWife = CreateThread(NULL, NULL, CreateFileProc, NULL, NULL, NULL);
    hHusband = CreateThread(NULL, NULL, OperationFileProc, NULL, NULL, NULL);
    WaitForSingleObject(hWife, -1);
    WaitForSingleObject(hHusband, -1);
}

代码的执行顺序是有问题的 

 加上了事件同步代码后

// 14_事件对象解决同步问题.cpp : 此文件包含 "main" 函数。程序执行将在此处开始并结束。
//

#include <iostream>
#include<Windows.h> 


HANDLE g_CreateEvent = NULL;

HANDLE g_OperationEvent = NULL;

DWORD WINAPI CreateFileProc(LPVOID lpThreadParameter)
{
    printf("打开文件\r\n");
    //打开文件成功
    SetEvent(g_CreateEvent);
    //等待获取文件大小
    WaitForSingleObject(g_OperationEvent, -1);
    printf("申请缓冲区\r\n");
    //缓冲区申请成功
    SetEvent(g_CreateEvent);
    //等待读写文件
    WaitForSingleObject(g_OperationEvent, -1);
    printf("关闭文件\r\n");
    SetEvent(g_CreateEvent);
    return 0;
}

DWORD WINAPI OperationFileProc(LPVOID lpThreadParameter)
{
    //等待打开文件
    WaitForSingleObject(g_CreateEvent, -1);
    printf("获取文件大小\r\n");
    //获取文件大小完成
    SetEvent(g_OperationEvent);
    //等待获取缓冲区
    WaitForSingleObject(g_CreateEvent, -1);
    printf("读写文件\r\n");
    //读写文件成功
    SetEvent(g_OperationEvent);
    return 0;
}
int main()
{
    HANDLE  hWife = NULL;
    HANDLE hHusband = NULL;
    g_CreateEvent = CreateEvent(
        NULL,  //安全属性
        FALSE, //TRUE:手工设置的事件对象
               //FALSE:自动设置的事件对象
        FALSE,  //TRUE:初始状态是激发态
               //FALSE:初始状态就是非激发态
        NULL
    );

    g_OperationEvent = CreateEvent(
        NULL,  //安全属性
        FALSE, //TRUE:手工设置的事件对象
               //FALSE:自动设置的事件对象
        FALSE,  //TRUE:初始状态是激发态
               //FALSE:初始状态就是非激发态
        NULL
    );



    hWife = CreateThread(NULL, NULL, CreateFileProc, NULL, NULL, NULL);
    hHusband = CreateThread(NULL, NULL, OperationFileProc, NULL, NULL, NULL);
    WaitForSingleObject(hWife, -1);
    WaitForSingleObject(hHusband, -1);
}

顺序正确

信号量

特点: 有信号数概念 只要信号数不为0 那么就处于激发态 waitfor函数对他的副作用就是将信号数减1,最大信号数为1的信号量可以解决互斥问题 最大信号量为1的时候和事件相同

函数作用备注
CreateSemaphore创建信号量

可以给信号量起名字

可以指定最大信号量数和当前信号数

OpenSemaphore打开信号量根据名字打开信号量
ReleaseSemaphore释放信号量会增加信号量的信号数,但不会超过最大信号数
WaitForSingleObject等待信号量处于激发态若处于激发态,则会减少一个信号数,信号数为0,将其设置为非激发态

// 15_信号量解决互斥问题.cpp : 此文件包含 "main" 函数。程序执行将在此处开始并结束。
//

#include <iostream>
#include <Windows.h>
int g_n;
HANDLE g_hSemaphore = NULL;
DWORD WINAPI ThreadPro1(LPVOID lpThreadParameter) {
    for (int i = 0; i < 100000; i++)
    {
        WaitForSingleObject(g_hSemaphore, -1);
        g_n++;
        ReleaseSemaphore(g_hSemaphore, 1, NULL);
    }

    return 0;
}
DWORD WINAPI ThreadPro2(LPVOID lpThreadParameter) {
    for (int i = 0; i < 100000; i++)
    {
        //激发态设置为非激发态 并且信号数减1
        WaitForSingleObject(g_hSemaphore, -1);
        g_n++;
        //释放一个信号数
        ReleaseSemaphore(g_hSemaphore, 1, NULL);
    }
    return 0;
}
int main() {
    HANDLE hThread1 = 0, hThread2;
    g_hSemaphore = CreateSemaphore(
        NULL,//安全属性
        1,   //初始信号数量
        1,   //最大信号数量
        NULL
    );
    hThread1 = CreateThread(NULL, NULL, ThreadPro1, NULL, NULL, NULL);
    hThread2 = CreateThread(NULL, NULL, ThreadPro2, NULL, NULL, NULL);
    WaitForSingleObject(hThread1, -1);
    WaitForSingleObject(hThread2, -1);
    printf("%d", g_n);
    return 0;
}

多个信号数的信号量 适用于解决多个线程之间有顺序的问题.最为经典的就是生产者消费者问题

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值