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

从零开始:手把手教你检查并更新英伟达GPU驱动和CUDA版本(Windows/Linux双平台)

本文详细指导如何在Windows和Linux系统上检查并更新英伟达GPU驱动和CUDA版本,解决版本不匹配导致的常见问题。涵盖驱动更新、CUDA Toolkit安装、版本冲突排解及性能优化技巧,帮助开发者高效管理GPU计算环境。

weixin_29237635的博客 701

华为MA5626 ONU开局配置实战:从零到业务就绪的完整命令手册

本文详细介绍了华为MA5626 ONU设备的开局配置全流程,包括物理连接、命令行初始化、业务VLAN划分、PoE功能配置及故障排查。通过实战命令手册,帮助网络工程师快速完成从设备上电到业务就绪的完整配置,特别适合FTTB场景部署。

weixin_26934905的博客 561

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

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

qq_32506555的博客 409

Dynamics 365 On-Premise Server v9.0部署体验

期盼已久的Dynamics 365 On-Premise的版本升级终于在11月初姗姗来迟。 11月2日起已经可以在官方渠道下载正式版的Dynamics 365 Server v9.0安装文件,版本号9.0.2,随手附上中文版安装文件下载地址。 想必已经有不少小伙伴开始尝试各种升级、安装了吧。 Dynamics 365 On-Premise的安装过程,度娘一下已经有很多了,这里就不赘述了,只是...

Roger的专栏 2139

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

Ut接口原理及 VoLTE 补充业务自管理信令流程(GBA 引导认证+业务访问认证+ VoLTE 补充业务自管理)

2. Ut 接口及 VoLTE 业务子管理概述 2.1 UT 接口 —— VoLTE 终端补充业务配置 2.2终端通过 Ut 接口完成 VoLTE 补充业务自管理举例 2.3什么是 GBA 和 GAA 架构 2.4为什么要引入 GBA 架构认证及 BSF 网元 2.5UE(终端) 接口相关网元及功能 2.6GBA 架构流程概述 3. Ut 接口业务自管理信令流程 3.1 补充业务自管理及本文主要参考以下规范 3.2 Ut 接口业务自管理信令流程:查询补充业务 3.2.1 UE获

原来45的博客 1257

Juniper SRX PPPoE配置

set interfaces pp0 unit 1 ppp-options chap default-chap-secret 拨号用户名密码。set interfaces pp0 unit 0 ppp-options chap default-chap-secret 拨号密码。set interfaces pp0 unit 0 ppp-options chap local-name 拨号用户名。6号口接运营商进行拨号。

Jian Sun_的博客 750

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

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

积累点滴,保持自我 2048

华为od面试手撕真题【分解质因数】

华为od面试手撕真题【分解质因数】

MISAYAONE的博客 1133

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

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

郭小明的专栏 346

ROS2 CANopen 使用教程

第一步是定义一个结构,用于保存有关接头和相关 CANopen 数据的信息。该结构是 CANopen 网络的基础,可确保存储所有相关数据并在需要时进行访问。

机器人控制与仿真应用 3690

基于Simulink的柔性关节人形机器人振动抑制控制仿真

本文完成了基于Simulink的柔性关节人形机器人振动抑制控制仿真,实现了:✅ 建立二质量柔性关节动力学模型✅ 设计加速度反馈主动阻尼控制器✅ 对比陷波滤波与状态反馈效果✅ 验证大幅运动下的快速振动衰减能力✅ 为高动态人形机器人操作(如击掌、搬运)提供平滑执行基础。

xiaoheshang_123的博客 129

[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

蓝牙模块HM-10手册

蓝牙模块HM-10手册,该手册对蓝牙模块HM-10进行了详细的介绍和使用方法以及一些AT指令。

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

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

feiyang5260的博客 1204

LinePhone3.2.7(android端最新版本) sip客户端

SIP客户端For android:LinePhone3.2.7(最新版本),官网地址:http://www.linphone.org/

锂电池充电器电路设计 里面有电路

锂电池充电器电路设计 里面有电路 3.7V升压到5V电路设计。

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

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

书山有路勤为径 1087

RU DOS版、EFI版32bit/64bit BIOS常用工具

DOS版、EFI版的RU工具,可用来查看IO、MEM、PCI、SMBIOS等资源。亲测在EFI SHELL下可用。

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、付费专栏及课程。

余额充值