C++线程同步的四种方式

C++的四种多线程同步机制 C++多线程同步机制摘要 C++提供了四种核心多线程同步机制来保证线程安全和资源共享: 互斥锁:通过独占访问保护共享资源,防止数据竞争。常用std::mutex配合RAII包装类(std::lock_guard/std::unique_lock)自动管理锁生命周期。 条件变量:实现线程间精准时序协调,通过等待/通知机制解决生产者-消费者问题。必须与互斥锁配合使用,注意处理虚假唤醒。 信号量:基于资源计数器控制并发访问数量,适合线程池等场景。C++20正式引入std::counting_semaphore。 阅读详情

线程之间通信的两个基本问题是互斥和同步。
       (1)线程同步是指线程之间所具有的一种制约关系,一个线程的执行依赖另一个线程的消息,当它没有得到另一个线程的消息时应等待,直到消息到达时才被唤醒。

        (2)线程互斥是指对于共享的操作系统资源(指的是广义的"资源",而不是Windows的.res文件,譬如全局变量就是一种共享资源),在各线程访问时的排它性。当有若干个线程都要使用某一共享资源时,任何时刻最多只允许一个线程去使用,其它要使用该资源的线程必须等待,直到占用资源者释放该资源。
  线程互斥是一种特殊的线程同步。
  实际上,互斥和同步对应着线程间通信发生的两种情况:
  (1)当有多个线程访问共享资源而不使资源被破坏时;
  (2)当一个线程需要将某个任务已经完成的情况通知另外一个或多个线程时。

    从大的方面讲,线程的同步可分用户模式的线程同步和内核对象的线程同步两大类。用户模式中线程的同步方法主要有原子访问和临界区等方法。其特点是同步速度特别快,适合于对线程运行速度有严格要求的场合。
    
    内核对象的线程同步则主要由事件、等待定时器、信号量以及信号灯等内核对象构成。由于这种同步机制使用了内核对象,使用时必须将线程从用户模式切换到内核模式,而这种转换一般要耗费近千个CPU周期,因此同步速度较慢,但在适用性上却要远优于用户模式的线程同步方式。

在WIN32中,同步机制主要有以下几种:
  (1)事件(Event);
  (2)信号量(semaphore);
  (3)互斥量(mutex);
  (4)临界区(Critical section)。

 

临界区

  临界区(Critical Section)是一段独占对某些共享资源访问的代码,在任意时刻只允许一个线程对共享资源进行访问。如果有多个线程试图同时访问临界区,那么在有一个线程进入后其他所有试图访问此临界区的线程将被挂起,并一直持续到进入临界区的线程离开。临界区在被释放后,其他线程可以继续抢占,并以此达到用原子方式操作共享资源的目的。

  临界区在使用时以CRITICAL_SECTION结构对象保护共享资源,并分别用EnterCriticalSection()和LeaveCriticalSection()函数去标识和释放一个临界区。所用到的CRITICAL_SECTION结构对象必须经过InitializeCriticalSection()的初始化后才能使用,而且必须确保所有线程中的任何试图访问此共享资源的代码都处在此临界区的保护之下。否则临界区将不会起到应有的作用,共享资源依然有被破坏的可能。

 

全局变量

  因为进程中的所有线程均可以访问所有的全局变量,因而全局变量成为Win32多线程通信的最简单方式。例如:

int var; //全局变量
UINT ThreadFunction(LPVOIDpParam)
{
 var = 0;
 while (var < MaxValue)
 {
  //线程处理
  ::InterlockedIncrement(long*) &var);
 }
 return 0;
}
请看下列程序:
int globalFlag = false; 
DWORD WINAPI ThreadFunc(LPVOID n)
{
 Sleep(2000);
 globalFlag = true;

 return 0;
}

int main()
{
 HANDLE hThrd;
 DWORD threadId;

 hThrd = CreateThread(NULL, 0, ThreadFunc, NULL, 0, &threadId);
 if (hThrd)
 {
  printf("Thread launched\n");
  CloseHandle(hThrd);
 }

 while (!globalFlag)
 ;
 printf("exit\n");
} 
  上述程序中使用全局变量和while循环查询进行线程间同步,实际上,这是一种应该避免的方法,因为: 
  (1)当主线程必须使自己与ThreadFunc函数的完成运行实现同步时,它并没有使自己进入睡眠状态。由于主线程没有进入睡眠状态,因此操作系统继续为它调度C P U时间,这就要占用其他线程的宝贵时间周期;
  (2)当主线程的优先级高于执行ThreadFunc函数的线程时,就会发生globalFlag永远不能被赋值为true的情况。因为在这种情况下,系统决不会将任何时间片分配给ThreadFunc线程。

 

事件
  事件(Event)是WIN32提供的最灵活的线程间同步方式,事件可以处于激发状态(signaled or true)或未激发状态(unsignal or false)。根据状态变迁方式的不同,事件可分为两类:
  (1)手动设置:这种对象只可能用程序手动设置,在需要该事件或者事件发生时,采用SetEvent及ResetEvent来进行设置。
  (2)自动恢复:一旦事件发生并被处理后,自动恢复到没有事件状态,不需要再次设置。
  使用"事件"机制应注意以下事项:
  (1)如果跨进程访问事件,必须对事件命名,在对事件命名的时候,要注意不要与系统命名空间中的其它全局命名对象冲突;
  (2)事件是否要自动恢复;
  (3)事件的初始状态设置。
  由于event对象属于内核对象,故进程B可以调用OpenEvent函数通过对象的名字获得进程A中event对象的句柄,然后将这个句柄用于ResetEvent、SetEvent和WaitForMultipleObjects等函数中。此法可以实现一个进程的线程控制另一进程中线程的运行,例如:
HANDLE hEvent=OpenEvent(EVENT_ALL_ACCESS,true,"MyEvent"); 
ResetEvent(hEvent);


信号量
  信号量是维护0到指定最大值之间的同步对象。信号量状态在其计数大于0时是有信号的,而其计数是0时是无信号的。信号量对象在控制上可以支持有限数量共享资源的访问。

  信号量的特点和用途可用下列几句话定义:
  (1)如果当前资源的数量大于0,则信号量有效;
  (2)如果当前资源数量是0,则信号量无效;
  (3)系统决不允许当前资源的数量为负值;
  (4)当前资源数量决不能大于最大资源数量。


  创建信号量
  HANDLE CreateSemaphore (
   PSECURITY_ATTRIBUTE psa,
   LONG lInitialCount, //开始时可供使用的资源数
   LONG lMaximumCount, //最大资源数
   PCTSTR pszName);


  释放信号量  
    通过调用ReleaseSemaphore函数,线程就能够对信标的当前资源数量进行递增,该函数原型为:
  BOOL WINAPI ReleaseSemaphore(
   HANDLE hSemaphore,
   LONG lReleaseCount, //信号量的当前资源数增加lReleaseCount
   LPLONG lpPreviousCount
   );  

 
       打开信号量  
  和其他核心对象一样,信号量也可以通过名字跨进程访问,打开信号量的API为:
  HANDLE OpenSemaphore (
   DWORD fdwAccess,
   BOOL bInherithandle,
   PCTSTR pszName
  );


  互锁访问
  当必须以原子操作方式来修改单个值时,互锁访问函数是相当有用的。所谓原子访问,是指线程在访问资源时能够确保所有其他线程都不在同一时间内访问相同的资源。
  请看下列代码:
int globalVar = 0;

DWORD WINAPI ThreadFunc1(LPVOID n)
{
 globalVar++;
 return 0;
}
DWORD WINAPI ThreadFunc2(LPVOID n)
{
 globalVar++;
 return 0;
} 
  运行ThreadFunc1和ThreadFunc2线程,结果是不可预料的,因为globalVar++并不对应着一条机器指令,我们看看globalVar++的反汇编代码:
00401038 mov eax,[globalVar (0042d3f0)]
0040103D add eax,1
00401040 mov [globalVar (0042d3f0)],eax   

在"mov eax,[globalVar (0042d3f0)]" 指令与"add eax,1" 指令以及"add eax,1" 指令与"mov [globalVar (0042d3f0)],eax"指令之间都可能发生线程切换,使得程序的执行后globalVar的结果不能确定。我们可以使用InterlockedExchangeAdd函数解决这个问题:

int globalVar = 0;

DWORD WINAPI ThreadFunc1(LPVOID n)
{
 InterlockedExchangeAdd(&globalVar,1);
 return 0;
}
DWORD WINAPI ThreadFunc2(LPVOID n)
{
 InterlockedExchangeAdd(&globalVar,1);
 return 0;
} 
  InterlockedExchangeAdd保证对变量globalVar的访问具有"原子性"。互锁访问的控制速度非常快,调用一个互锁函数的CPU周期通常小于50,不需要进行用户方式与内核方式的切换(该切换通常需要运行1000个CPU周期)。
  互锁访问函数的缺点在于其只能对单一变量进行原子访问,如果要访问的资源比较复杂,仍要使用临界区或互斥。


可等待定时器  

        可等待定时器是在某个时间或按规定的间隔时间发出自己的信号通知的内核对象。它们通常用来在某个时间执行某个操作。

  创建可等待定时器
        HANDLE CreateWaitableTimer(
           PSECURITY_ATTRISUTES psa,
           BOOL fManualReset,//人工重置或自动重置定时器
             PCTSTR pszName);


  设置可等待定时器  

       可等待定时器对象在非激活状态下被创建,程序员应调用 SetWaitableTimer函数来界定定时器在何时被激活:
       BOOL SetWaitableTimer(
           HANDLE hTimer, //要设置的定时器
           const LARGE_INTEGER *pDueTime, //指明定时器第一次激活的时间
           LONG lPeriod, //指明此后定时器应该间隔多长时间激活一次
           PTIMERAPCROUTINE pfnCompletionRoutine,
           PVOID PvArgToCompletionRoutine,
             BOOL fResume);


  取消可等待定时器
        BOOL Cancel WaitableTimer(
              HANDLE hTimer //要取消的定时器
         );  

 

        打开可等待定时器 

        作为一种内核对象,WaitableTimer也可以被其他进程以名字打开:

         HANDLE OpenWaitableTimer (
              DWORD fdwAccess,
              BOOL bInherithandle,
              PCTSTR pszName
         );

 
  实例
  下面给出的一个程序可能发生死锁现象:
#include <windows.h>
#include <stdio.h>
CRITICAL_SECTION cs1, cs2;
long WINAPI ThreadFn(long);
main()
{
 long iThreadID;
 InitializeCriticalSection(&cs1);
 InitializeCriticalSection(&cs2);
 CloseHandle(CreateThread(NULL, 0, (LPTHREAD_START_ROUTINE)ThreadFn, NULL, 0,&iThreadID));
 while (TRUE)
 {
  EnterCriticalSection(&cs1);
  printf("\n线程1占用临界区1");
  EnterCriticalSection(&cs2);
  printf("\n线程1占用临界区2");

  printf("\n线程1占用两个临界区");

  LeaveCriticalSection(&cs2);
  LeaveCriticalSection(&cs1);

  printf("\n线程1释放两个临界区");
  Sleep(20);
 };
 return (0);
}

long WINAPI ThreadFn(long lParam)
{
 while (TRUE)
 {
  EnterCriticalSection(&cs2);
  printf("\n线程2占用临界区2");
  EnterCriticalSection(&cs1);
  printf("\n线程2占用临界区1");

  printf("\n线程2占用两个临界区");

  LeaveCriticalSection(&cs1);
  LeaveCriticalSection(&cs2);

  printf("\n线程2释放两个临界区");
  Sleep(20);
 };
} 
  运行这个程序,在中途一旦发生这样的输出:
  线程1占用临界区1  线程2占用临界区2
  或
  线程2占用临界区2  线程1占用临界区1
  或
  线程1占用临界区2  线程2占用临界区1
  或
  线程2占用临界区1  线程1占用临界区2
  程序就"死"掉了,再也运行不下去。因为这样的输出,意味着两个线程相互等待对方释放临界区,也即出现了死锁。
  如果我们将线程2的控制函数改为:
long WINAPI ThreadFn(long lParam)
{
 while (TRUE)
 {
  EnterCriticalSection(&cs1);
  printf("\n线程2占用临界区1");
  EnterCriticalSection(&cs2);
  printf("\n线程2占用临界区2");

  printf("\n线程2占用两个临界区");

  LeaveCriticalSection(&cs1);
  LeaveCriticalSection(&cs2);

  printf("\n线程2释放两个临界区");
  Sleep(20);
 };
} 
  再次运行程序,死锁被消除,程序不再挡掉。这是因为我们改变了线程2中获得临界区1、2的顺序,消除了线程1、2相互等待资源的可能性。
  由此我们得出结论,在使用线程间的同步机制时,要特别留心死锁的发生。

Linux C++多线程同步的四种方式(非常详细) 背景问题:在特定的应用场景下,多线程不进行同步会造成什么问题?通过多线程模拟多窗口售票为例:#include <iostream> #include<pthread.h> #include<stdio.h> #include<stdlib.h> #include<string.h> #include<unistd.h> us... 阅读详情

相关推荐

C++ 多线程同步的四种常见方式

在 C++ 中,可以使用 std::mutex 类定义一个互斥锁对象,在访问共享资源时使用 std::lock_guard 对象进行加锁和解锁。本文介绍了 C++ 中常用的四种线程同步方式,包括互斥锁、条件变量、信号量和原子变量,并给出了相应的示例代码。在实际的多线程编程中,根据不同的需求和场景选择合适的线程同步方式,可以提高程序的性能和安全性。在 C++ 的多线程编程中,线程同步是一项非常重要的任务。条件变量是一种等待-通知的机制,它可以让线程在某个条件满足时等待,直到其他线程通知它继续执行。

DevNinja的博客 684

【C++】多线程同步

线程间的同步方法大体可以分为两类:用户模式和内核模式。内核模式就是指利用系统内核对象的单一性来进行同步,使用时需要切换内核态与用户态,而用户模式就是不需要切换到内核态,只在用户态完成操作。 用户模式下的方法有:原子操作(例如一个单一的全局变量)、临界区。特点是:同步速度特别快。 内核模式下的方法有:事件、信号量、互斥量。同步速度较慢,但适用性比较好。 **临界区:**通过对多线程的串行化来访问公共资源或一段代码、速度快,适合控制数据访问。 **互斥量:**为协调共同对一个共享资源的单独访问而设计的。 *

目标:MVP 7629

C++线程同步几种方法

在C++中,线程同步是确保多个线程在访问共享资源时能够协调一致的重要概念。

yy_xzz的博客 1059

C++多线程同步

摘要:本文介绍了C++11中如何开启新线程,并详细讲解了线程的基础同步原语:mutex, lock_guard, unique_lock, condition variable和semaphore等。如何采用async, packaged_task和promise实现future同步机制?怎样处理spurious wakeup?本文以质数判定服务为例为大家分享C++多线程同步措施! 1. C++线程和基础同步原语 Thread mutex, lock_guard, unique_lock con.

书山有路勤为径 5529

从宏观到街道:如何利用七普数据为WorldPop人口栅格做“本地化”校准?

本文探讨了如何利用第七次全国人口普查(七普)数据对WorldPop人口栅格进行本地化校准,以提高街道级别的人口分布精度。通过分析数据偏差、选择适当的技术路线和工具(如Arcgis和栅格计算器),文章详细介绍了从宏观到街道的校准实践,帮助规划公共卫生资源和优化城市基础设施。

janckywong963的专栏 513

DiffusionDrive实战:如何用截断扩散模型3个月搞定自动驾驶轨迹规划(附代码)

本文详细介绍了如何利用DiffusionDrive截断扩散模型,在三个月内快速构建端到端的自动驾驶轨迹预测系统。内容涵盖从环境搭建、数据处理、模型架构解析到训练调参和推理优化的完整实战流程,并提供了关键代码示例,旨在帮助开发者高效实现高质量的自动驾驶轨迹规划。

weixin_35578748的博客 147

C++ 线程同步的四种方式

在C++程序开发中,线程之间通信的两个基本问题是互斥和同步:   (1)线程同步是指线程之间所具有的一种制约关系,一个线程的执行依赖另一个线程的消息,当它没有得到另一个线程的消息时应等待,直到消息到达时才被唤醒。   (2)线程互斥是指对于共享的操作系统资源(指的是广义的”资源”,而不是Windows的.res文件,譬如全局变量就是一种共享资源),在各线程访问时的排它性。当有若干个线程

qq_32506555的博客 409

W5500 MACRAW模式实战:在ESP32上抓取并解析原始以太网数据包

本文详细介绍了如何在ESP32开发板上使用W5500芯片的MACRAW模式捕获和解析原始以太网数据包。通过硬件连接指南、代码实现示例和协议解析技巧,帮助开发者深入理解网络通信底层机制,实现网络嗅探和自定义协议开发等高级应用。

weixin_42520573的博客 418

Linux C++多线程同步的四种方式

原文链接:Linux C++多线程同步的四种方式(非常详细)_Y先森0.0-优快云博客 背景问题:在特定的应用场景下,多线程不进行同步会造成什么问题? 通过多线程模拟多窗口售票为例: #include <iostream> #include<pthread.h> #include<stdio.h> #include<stdlib.h> #include<string.h> #include<unistd.h> using n

qq_41687938的博客 993

【YOLO系列】YOLOv12详解:模型结构、损失函数、训练方法及代码实现

本文提出YOLOv12,一种以注意力为中心的实时目标检测框架,通过创新性改进解决传统YOLO模型在精度与速度上的失衡问题。核心贡献包括:1)轻量混合注意力模块(LHA)结合通道与空间注意力,在不增加计算负担下提升特征提取能力;2)区域注意机制与R-ELAN网络优化特征聚合,显著改善小目标检测(AP_S提升8.3%);3)动态多光谱数据增强策略增强模型鲁棒性。实验表明,YOLOv12在COCO等基准上实现SOTA性能,同时保持2.1 TOPS/W的高能效比,适用于边缘设备部署。该工作为实时目标检测提供了新的注

一碗白开水一 1808

前端build工程的时候 出现 `build.rollupOptions.external 的解决方法

看到这个Bug,理性分析: 遇到 Rollup failed to resolve import “@vue/devtools-api” 错误可能是由于 Pinia 使用了 @vue/devtools-api 模块,而 Vite 的 Rollup 插件无法解析这个模块 ps:但是我前一秒还可以,加了个别代码,突然不行,我是不相信的 = - =

码农研究僧的博客 3055

C++线程同步的四种方式(Windows)

一、为什么要进行线程同步? 在程序中使用多线程时,一般很少有多个线程能在其生命期内进行完全独立的操作。更多的情况是一些线程进行某些处理操作,而其他的线程必须对其处理结果进行了解。正常情况下对这种处理结果的了解应当在其处理任务完成后进行。如果不采取适当的措施,其他线程往往会在线程处理任务结束前就去访问处理结果,这就很有可能得到有关处理结果的错误了解。例如,多个线程同时访问同一个全局变量,如果都是读取操作,则不会出现问题。如果一个线程负责改变此变量的值,而其他线程负责同时读取变量内容,则不能保证读取到的数据是

积累点滴,保持自我 2048

C++实现线程同步的四种方式总结

这两种事件对象的区别在于当人工重置的事件对象得到通知时,等待该事件对象的所有线程均变为可调度线程;而当一个自动重置的事件对象得到通知时,等待该事件对象的线程中只有一个线程变为可调度线程。调用EnterCriticalSection函数,以获得指定的临界区对象的所有权,该函数等待指定的临界区对象的所有权,如果该所有权赋予了调用线程,则该函数就返回;当临界区不再需要时,可以调用DeleteCriticalSection函数释放该对象,该函数将释放一个没有被任何线程所拥有的临界区对象的所有资源。

郭小明的专栏 346

深度强化学习 | 基于SAC算法的动态避障(ROS C++仿真)

本文介绍了基于SAC(软性演员-评论家)算法的机器人运动规划方法,通过PyTorch框架实现了端到端训练,并部署到Python和ROS C++中。实验结果显示,基于SAC算法的智能体在动态避障任务中表现良好,能实现从起点到终点的无碰撞导航。

FRIGIDWINTER的博客 8210

【WRFDA安装第一期】安装 WRFDA 以运行 3DVAR/4DVAR数据同化

【WRFDA安装第一期】安装 WRFDA 以运行 3DVAR/4DVAR数据同化

WW、forever的博客 1895

[C++] 线程同步的四种方式和demo

源码路径:dangwei-90/ThreadSync: 线程同步 (github.com) (https://github.com/dangwei-90/ThreadSync) 编译平台:win10 编译工具:vs2019 语音: C++ 下边直接看demo,一目了然。 1.线程未同步的demo // thread_sync.cpp : 此文件包含 "main" 函数。程序执行将在此处开始并结束。 // #include <iostream> #include &l...

就是那个党伟 602

TMS_WebGMaps_1.9.8.0_Full_Source

TMS_WebGMaps_1.9.8.0_Full_Source 全源码 delphi cbuilder

笔记:c++四种线程同步方式

1.为什么线程同步 比如多个线程同时访问一个全局变量,如果是读取则没问题,如果某个线程改变此变量的值,与此同时其他线程读取该变量值时,不能保证读取到的数据是不是经过那个写线程修改过的。 为了确保获取到的数据是经过线程修改过的,就必须在线程写入变量时,禁止其它线程对此变量做任何访问,直到数据写入完成之后再接触对其它线程的访问限制,这就是线程同步。 2.线程同步与互...

feiyang5260的博客 1204

计算机控制技术及应用

《计算机控制技术及应用》通过总结作者的科研成果和吸收国内外的先进理论、方法和技术,以计算机控制系统的网络化、开放化、智能化和集成化发展趋势为主线索,主要介绍计算机控制系统的基本概念和计算机控制系统的典型形式,计算机控制系统的数学描述与基本分析方法,以及数字PID控制器、最少拍控制系统、达林算法、状态变量设计法等设计方法及其各种改进算法。重点突出计算机控制系统输入输出通道接口的设计方法和典型控制系统的设计案例与实现方法等。

ET2012新版免狗免安装绿色软件

ET2012新版免狗免安祖昂绿色软件。,新界面,新视觉,

Linux C++实现多线程同步的四种方式

https://www.bilibili.com/read/cv4323139 背景问题:在特定的应用场景下,多线程不进行同步会造成什么问题? 通过多线程模拟多窗口售票为例: #include <iostream> #include<pthread.h> #include<stdio.h> #include<stdlib.h> #include...

书山有路勤为径 1087

文档:亲测可用解决vnc灰屏,黑屏,鼠标大黑叉情况

主要解决vnc-viewer灰屏,黑屏,鼠标大黑叉等现象,详细见文档。

C++进阶—>线程同步的四种方式

线程之间通信的两个基本问题是互斥和同步。 (1)线程同步是指线程之间所具有的一种制约关系,一个线程的执行依赖另一个线程的消息,当它没有得到另一个线程的消息时应等待,直到消息到达时才被唤醒。 (2)线程互斥是指对于共享的操作系统资源(指的是广义的”资源”,而不是Windows的.res文件,譬如全局变量就是一种共享资源),在各线程访问时的排它性。当有若干个线程都要使用某一共享资源时,任何

小伟锅的博客 1033
上一篇: SSH框架整合之注册登录
下一篇: Win32编写窗口程序的步骤
丶CrazyPanda
博客等级 码龄17年 18粉丝 7原创
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

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

抵扣说明:

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

余额充值