0


【Windows线程开发】Windows线程同步技术

我们在上一章节中讲解了关于Windows的线程基础,相信大家已经对线程有了基本的概念。这一章节中,我们来讲讲线程同步技术,包括加锁技术(原子锁和互斥体)和事件,信号量。

文章目录

一.原子锁

原子锁主要解决的问题是多线程在操作符方面的问题。

  • 相关问题: > 多个线程对同一个数据进行原子操作时,会产生结果丢失,比如++运算符

我们来写一段代码看看多线程在操作同一个数据的时候出现的问题:

#include<stdio.h>#include<windows.h>

DWORD WINAPI ThreadProc1(LPVOID lpParameter);
DWORD WINAPI ThreadProc2(LPVOID lpParameter);int g_value =0;intmain(){
    DWORD nID =0;
    HANDLE hThread1 =CreateThread(NULL,0, ThreadProc1,NULL,0,&nID);
    HANDLE hThread2 =CreateThread(NULL,0, ThreadProc2,NULL,0,&nID);WaitForMultipleObjects(2, hThread, TRUE, INFINITE);printf("%d\n", g_value);return0;}

DWORD WINAPI ThreadProc1(LPVOID lpParameter){for(int i =0; i <100000000; i++){
        g_value++;}return0;}

DWORD WINAPI ThreadProc2(LPVOID lpParameter){for(int i =0; i <100000000; i++){
        g_value++;}return0;}
  • 代码解释 我们创建两个线程,同时对全局变量g_value进行自增操作,两个线程分别自增100000000次,那么最后结果就应该是200000000,我们来看看执行结果:原子锁相关问题 我们发现,最后结果并不是200000000,那么是为什么呢?

我们来分析一下错误:
当线程A执行g_value++时,如果线程切换正好是在线程A将结果保存到g_value之前,线程B继续执行g_value++,那么当线程A再次被切换回来之后,会继续上一步的操作,继续将值保存到g_value中,线程B的计算结果被覆盖

通俗点来说,就是线程A计算好了g_value的结果,但是还没有保存到g_value,这时候线程切换到了B线程,线程B完成了计算,并且成功保存,当返回到A线程的时候,A线程会继续上一步的保存操作,那么B线程的计算结果就被覆盖掉了。

那么如何来解决这样的问题呢?那就要用到我们的线程同步技术—原子锁了:

  • 原子锁函数:InterlockedIncrement()``````InterlockedDecrement()``````InterlockedCompareExcahnge()``````InterlockedExchange() 我们在上文中提到,原子锁主要针对的是运算符的问题,每一种运算符都有原子锁函数 我们来看看使用效果:这里以++运算符为例:
#include<stdio.h>#include<windows.h>

DWORD WINAPI ThreadProc1(LPVOID lpParameter);
DWORD WINAPI ThreadProc2(LPVOID lpParameter);
DWORD g_value =0;intmain(){
    DWORD nID =0;
    HANDLE hThread[2]={0};
    hThread[0]=CreateThread(NULL,0, ThreadProc1,NULL,0,&nID);
    hThread[1]=CreateThread(NULL,0, ThreadProc2,NULL,0,&nID);WaitForMultipleObjects(2, hThread, TRUE, INFINITE);printf("%d\n", g_value);return0;}

DWORD WINAPI ThreadProc1(LPVOID lpParameter){for(int i =0; i <100000000; i++){InterlockedIncrement(&g_value);}return0;}

DWORD WINAPI ThreadProc2(LPVOID lpParameter){for(int i =0; i <100000000; i++){InterlockedIncrement(&g_value);}return0;}

我们来看看执行效果:
原子锁执行效果2
我们可以发现,当我们使用原子锁的时候,两个线程操作同一个数据,就不会出现结果丢失的问题了。但是我们也不难发现,执行结果慢了很多,这是因为执行过程中多了很多等待事件,这个等待我们在互斥中会讲到。
原子锁的实现:直接对数据所在的内存操作,并且在任何一个瞬间,只能有一个线程访问

二.互斥体

  • 相关问题 跟原子锁一样,都是解决多线程下资源的共享使用,但是与原子锁不同的是,互斥体解决的是代码资源的共享使用。
  • 互斥体的使用: - 1. 创建互斥体: 使用CreateMutex函数: MSDN官方文档解释
HMODLE CreateMutex(
    LPSECURITY_ATTRIBUTES lpMutexAttributes,//安全性
    BOOL bInitialOwner,//初始拥有者
    LPCSTR lpName              //为互斥命名);

参数bInitialOwner介绍:
如果此值为 TRUE ,并且调用方创建了互斥体,则调用线程获取互斥体对象的初始所有权。 否则,调用线程不会获取互斥体的所有权。

互斥体特性介绍:

  1. 在任何一个时间点上,只能由一个线程拥有互斥体
  2. 当前任何一个线程不拥有互斥体是,互斥体句柄有信号
  3. 谁先等候互斥体,谁先获取
    1. 等候互斥体: 上一篇介绍过了,使用等候句柄函数。WaitFor...
    1. 释放互斥体
BOOL ReleaseMutex(
    HANDLE hMutex           //handle of Mutex);
    1. 关闭互斥体 使用CloseHandle函数 我们来看看使用互斥体来解决我们在多线程中遇到的问题:
#include<stdio.h>#include<windows.h>

DWORD WINAPI ThreadProc1(LPVOID lpParameter);
DWORD WINAPI ThreadProc2(LPVOID lpParameter);
DWORD g_value =0;
HANDLE hMutex =NULL;//用于接收互斥体句柄intmain(){
    DWORD nID =0;
    HANDLE hThread[2]={0};//hMutex = CreateMutex(NULL, FALSE, NULL);
    hThread[0]=CreateThread(NULL,0, ThreadProc1,NULL,0,&nID);
    hThread[1]=CreateThread(NULL,0, ThreadProc2,NULL,0,&nID);WaitForMultipleObjects(2, hThread, TRUE, INFINITE);printf("%d\n", g_value);CloseHandle(hMutex);return0;}

DWORD WINAPI ThreadProc1(LPVOID lpParameter){char a[]="********";while(1){//WaitForSingleObject(hMutex, INFINITE);for(int i =0; i <strlen(a); i++){printf("%c", a[i]);Sleep(125);}printf("\n");//ReleaseMutex(hMutex);}return0;}

DWORD WINAPI ThreadProc2(LPVOID lpParameter){char b[]="--------";while(1){//WaitForSingleObject(hMutex, INFINITE);for(int i =0; i <strlen(b); i++){printf("%c", b[i]);Sleep(125);}//ReleaseMutex(hMutex);printf("\n");}return0;}

我们来看看不适用互斥体技术的时候的输出:
不适用互斥体技术
我们再来看看使用了互斥体之后:

#include<stdio.h>#include<windows.h>

DWORD WINAPI ThreadProc1(LPVOID lpParameter);
DWORD WINAPI ThreadProc2(LPVOID lpParameter);
DWORD g_value =0;
HANDLE hMutex =NULL;//用于接收互斥体句柄intmain(){
    DWORD nID =0;
    HANDLE hThread[2]={0};
    hMutex =CreateMutex(NULL, FALSE,NULL);
    hThread[0]=CreateThread(NULL,0, ThreadProc1,NULL,0,&nID);
    hThread[1]=CreateThread(NULL,0, ThreadProc2,NULL,0,&nID);WaitForMultipleObjects(2, hThread, TRUE, INFINITE);printf("%d\n", g_value);CloseHandle(hMutex);return0;}

DWORD WINAPI ThreadProc1(LPVOID lpParameter){char a[]="********";while(1){WaitForSingleObject(hMutex, INFINITE);for(int i =0; i <strlen(a); i++){printf("%c", a[i]);Sleep(125);}printf("\n");ReleaseMutex(hMutex);}return0;}

DWORD WINAPI ThreadProc2(LPVOID lpParameter){char b[]="--------";while(1){WaitForSingleObject(hMutex, INFINITE);for(int i =0; i <strlen(b); i++){printf("%c", b[i]);Sleep(125);}ReleaseMutex(hMutex);printf("\n");}return0;}

使用互斥体
我们可以发现使用互斥体之后,对代码段进行了枷锁。
我们来大致讲解一下互斥体的实现吧:

我们在主进程中创建了互斥体,并且互斥体不归之进程所有,两个线程谁先等待互斥体句柄,谁就拥有了互斥体,那么当线程跳转到另一个线程之后,发现被锁定在了另一个线程,那么线程就会被阻塞,直到线程再次跳转到另一个线程,执行完之后,互斥体被释放,这时候跳转到这个线程,在这个线程中再进行加锁,这个线程执行完之后,再锁定到另一个线程,这样就实现了加锁技术。

三.事件

前两个技术都属于加锁技术,即两个线程互斥的时候使用,那么线程也会有协调工作的时候,这时候就需要用到我们的事件和信号量了。

  • 相关问题: 多线程协调工作的时候的通知问题
  • 事件的使用- 创建事件: 使用CreatEvent函数,MSDN官方解释
HANDLE CreateEvent(
    LPSECURITY_ATTRIBUTES lpEventAttributes,//安全属性
    BOOL                  bManualReset,//事件重置/复位方式
    BOOL                  bInitialState,//事件初始状态
    LPCSTR                lpName  //为事件命名);
  • 参数解释:
  • bManualReset为事件重置/复位方式,如果该参数被设置为TRUE那么就需要我们来手动重置事件对象,如果该参数被设置为FALSE,那么操纵系统会帮我们完成事件的重置和复位。
  • bInitialState:该参数指定了当创建事件后,该事件句柄是否处于有消息状态
  • 等候事件:WaitFor......函数
  • 触发事件(使事件句柄处于有消息状态)
BOOLSetEvent(
    HANDLE hEvent
);
  • 复位事件(将事件句柄设置为无消息状态)
BOOL ResetEvent(
    HANDLE hEvent
);
  • 关闭事件CloseHandle函数 我们来看看事件的使用:
#include<stdio.h>#include<windows.h>

DWORD WINAPI ThreadProc1(LPVOID lpParameter);
DWORD WINAPI ThreadProc2(LPVOID lpParameter);
DWORD g_value =0;
HANDLE hEvent =NULL;//用于接收事件句柄intmain(){
    DWORD nID =0;
    HANDLE hThread[2]={0};
    hEvent =CreateEvent(NULL,FALSE,0,NULL);
    hThread[0]=CreateThread(NULL,0, ThreadProc1,NULL,0,&nID);
    hThread[1]=CreateThread(NULL,0, ThreadProc2,NULL,0,&nID);WaitForMultipleObjects(2, hThread, TRUE, INFINITE);printf("%d\n", g_value);CloseHandle(hEvent);return0;}

DWORD WINAPI ThreadProc1(LPVOID lpParameter){char a[]="********";while(1){WaitForSingleObject(hEvent, INFINITE);for(int i =0; i <strlen(a); i++){printf("%c", a[i]);}printf("\n");ResetEvent(hEvent);}return0;}

DWORD WINAPI ThreadProc2(LPVOID lpParameter){while(1){Sleep(1000);SetEvent(hEvent);}return0;}

这是一个很典型的相互协调工作的双线程,我们在A线程中没有设定时间间隔,但是在B线程中设定了事件间隔,我们能够很明显地感受到输出是有时间间隔的:
事件处理

四.信号量

  • 相关问题: 类似于事件,解决线程之间通知的相关问题,但提供一个计数器,可以设置次数。
  • 创建信号量: 使用CreateSemaphore函数: MSDN官方解释
HANDLE CreateSemaphore(
    LPSECURITY_ATTRIBUIES lpSemaphoreAttributes,//安全属性
    LONG lInitialCount,//初始化信号量数量
    LONG lMaximumCount,//信号量的最大值
    LPSTSTR lpName             //为信号量命名);创建成功返回信号量句柄
  • 等候信号量 使用WaitFor...函数注意: 等候每通过一次,信号量的信号减一,知道为0阻塞
  • 给信号量指定定计数值 使用ReleaseSemaphore()函数 MSDN官方解释
BOOL ReleaseSemaphore(
    HANDLE hSeamephore,//信号量句柄
    LONG lReleaseSemaphore,//信号量将增加的量
    LPONG lpPreviousCount         //指向一个变量的指针,用于记录信号量的上一个计数);
  • 关闭信号量: 使用CloseHandle()函数 我们来看看信号量的使用实例:
#include<stdio.h>#include<windows.h>

DWORD WINAPI ThreadProc1(LPVOID lpParameter);
DWORD WINAPI ThreadProc2(LPVOID lpParameter);
HANDLE hSemaphore =NULL;//用于接收事件句柄intmain(){
    DWORD nID =0;
    HANDLE hThread[2]={0};
    hSemaphore =CreateSemaphore(NULL,3,10,NULL);
    hThread[0]=CreateThread(NULL,0, ThreadProc1,NULL,0,&nID);while(getchar()=='\n'){ReleaseSemaphore(hSemaphore,5,NULL);}CloseHandle(hSemaphore);return0;}

    DWORD WINAPI ThreadProc1(LPVOID lpParameter){char a[]="********";while(1){WaitForSingleObject(hSemaphore, INFINITE);for(int i =0; i <strlen(a); i++){printf("%c", a[i]);}printf("\n");}return0;}

我们设置了计数器为3的信号量,我们发现程序最开始只会输出三行,每当我们按一次回车键,就将信号量计数值值为5:
信号量实例
本篇文章的分享就到这里,如果大家发现有错误之处,还请大家指出来,我会非常虚心地学习。希望我们共同进步!!!

标签: windows c++ 算法

本文转载自: https://blog.csdn.net/qq_73985089/article/details/130646436
版权归原作者 WdIg-2023 所有, 如有侵权,请联系我们删除。

“【Windows线程开发】Windows线程同步技术”的评论:

还没有评论