[多线程] 多线程的通信和锁

多线程之间的通信

通过锁同步共享内容

因为一个进程中的多个线程是贡献进程的资源的,所以多线程可以通过访问进程中的全局变量来通信,为了避免竞争需要加锁。

线程中的通信,因为存在共享的进程资源,所以主要是要进行线程的同步(即各种方式的加锁)。
关于几种常见的锁,写在部分。←这也是这篇文章主要记录的内容。

消息

Windows中,可以通过PostThreadMessage()或者SendThreadMessage()来进行线程间通信。这个需要对Windows的消息机制(消息队列、消息循环等)有基本了解。
参考:
使用PostThreadMessage在Win32线程间传递消息

多线程常用的锁

互斥锁

互斥锁,即std::mutex,对于一个资源,同时只能有一个线程进行访问。
当一个线程持有互斥锁的时候,其余需要获取互斥锁的线程会进入睡眠, 不再占用cpu。当持有锁的线程解锁后,所有等待锁的线程全部激活,其中一个线程获得锁后,其余线程再次陷入睡眠。

互斥锁是最容易理解和使用的锁。

示例代码:

std::mutex mtx;
int data = 0;

void _func(int index)
{
    char msg[256] = { 0x00 };
    //std::lock_guard<std::mutex> lkg(mtx);
    mtx.lock();
    ZeroMemory(msg, sizeof(msg));
    sprintf_s(msg, 256, "%s%d%s", "this is ", std::this_thread::get_id(), " thread in.");
	std::cout << msg << std::endl;

    data = index;
    std::cout << data << std::endl;

    ZeroMemory(msg, sizeof(msg));
    sprintf_s(msg, 256, "%s%d%s", "this is ", std::this_thread::get_id(), " thread out.");
    std::cout << msg << std::endl;
    mtx.unlock();
    return;
}

int main()
{
    std::thread th1(_func,1);
    std::thread th2(_func,2);
    std::thread th3(_func,3);
    th1.join();
    th2.join();
    th3.join();
    return 0;
}

输出结果:

this is 7816 thread in.
3
this is 7816 thread out.
this is 18136 thread in.
1
this is 18136 thread out.
this is 13060 thread in.
2
this is 13060 thread out.

条件变量

条件变量std::condition_variable ,条件变量是和std::mutex 一起使用的。
等于是对互斥锁的升级,当一个持有互斥锁的线程解锁时,其余线程全部激活,去抢占互斥锁。
条件变量多使用于生产者-消费者这样的模型,生产者控制条件变量的notify,可以选择通知任意一个线程,还是通知所有线程。
例如以下代码:

std::mutex mtx;
std::condition_variable cdn;
int data = 0;
bool exit_threads = 0; //消费者函数_func中循环的退出条件

void _func_manager(int index) //生产者函数
{
    Sleep(1000);
    char msg[256] = { 0x00 };
    ZeroMemory(msg, sizeof(msg));
    sprintf_s(msg, 256, "%s%d%s", "this is mgr  ", std::this_thread::get_id(), " thread in.");    
    std::cout << msg << std::endl;
    int i = 0;
    while (i < 10)
    {
        i++;
        cdn.notify_one(); // 不断的激活消费者线程
        Sleep(200);//为了避免,消费者线程正在处理,接收不到notity,这里等待一下
    }
    exit_threads = true; //改变消费者线程循环退出条件
    cdn.notify_all(); //通知所有线程,让所有消费者线程进行循环条件检测
    ZeroMemory(msg, sizeof(msg));
    sprintf_s(msg, 256, "%s%d%s", "this is mgr  ", std::this_thread::get_id(), " thread out.");
    std::cout << msg << std::endl;
    return;
}

void _func(int index) //消费者线程函数
{
    char msg_in[256] = { 0x00 };
    char msg_out[256] = { 0x00 };
    ZeroMemory(msg_in, sizeof(msg_in));
    ZeroMemory(msg_out, sizeof(msg_out));
    sprintf_s(msg_in, 256, "%s%d%s", "this is ", std::this_thread::get_id(), " thread active.");
    sprintf_s(msg_out, 256, "%s%d%s", "this is ", std::this_thread::get_id(), " thread out.");
    while (!exit_threads) // 循环,只要循环退出条件没有达到,就会持续循环
    {
        std::unique_lock<std::mutex> ulk(mtx); // 创建unique_lick对象,unique_lock 生成即加锁
        cdn.wait(ulk); //条件变量等待,传入unique_lock对象,对unique_lock对象解锁。
        //当条件变量被通知时,再对unique_lock对象加锁。
        if (!exit_threads) { // 此时unicque_lock已自动加锁。再次检测循环退出条件
            std::cout << msg_in << std::endl;
            data++;
            std::cout << data << std::endl;
        }
    } //循环退出,结束线程
    std::cout << msg_out << std::endl;  
    return;
}

int main()
{
    std::thread mgr(_func_manager, 0);
    std::thread th1(_func, 1);
    std::thread th2(_func, 2);
    std::thread th3(_func, 3);
    mgr.join(); // join,主线程等待子线程结束,再执行join后面的内容。
    th1.join();
    th2.join();
    th3.join();
    return 0;
}

条件变量的wait有两个重载。

//MSVC\14.35.32215\include\mutex
void wait(unique_lock<mutex>& _Lck) { // wait for signal
        // Nothing to do to comply with LWG-2135 because std::mutex lock/unlock are nothrow
        _Cnd_wait(_Mycnd(), _Lck.mutex()->_Mymtx());
    }

    template <class _Predicate>
    void wait(unique_lock<mutex>& _Lck, _Predicate _Pred) { // wait for signal and test predicate
        while (!_Pred()) {
            wait(_Lck);
        }
    }

第一个重载函数,参数只需要一个unique_lock,但是可能存在虚假唤醒的问题,需要在wait 返回后,再通过条件判断,避免虚假唤醒。
第二个重载函数,需要两个参数,第一个是unique_lock,第二个可以传入一个Lambda表达式。当wait被唤醒时,会通过Lambda表达式判断,如果Lambda返回值为false,wait会unlock unique_lock并继续阻塞等待,返回值为true时,wait才继续执行。通过两个参数的重载,可以直接把避免虚假唤醒的判断,添加到wait函数上。

自旋锁

如果使用互斥锁,没有获得锁的线程,会进入休眠,不会占用cpu。
如果使用自旋锁,没有获得锁的线程,不会进入休眠,会不断尝试获取锁,会一直占据cpu。

使用互斥锁时,因为线程会休眠,cpu会切换去其他线程了,所以会有cpu切换的时间。
如果锁定的临界区执行的时间大于线程切换的时间,那使用互斥锁就可以。
如果锁定的临界区执行的事件很小,比线程切换的时间还小,那使用互斥锁所带来的线程切换时间就变成了很大的负担,就应该使用一直占用cpu的自旋锁。
//第是目前为止,我还不知道怎么看什么时候代码的执行时间小于线程切换时间。

自旋锁的实现原理

c++之理解自旋锁
这里提供了一种自旋锁的实现原理。主要是利用原子操作。
自旋锁监控的是锁对象中的atomic bool值flag。当没有加锁时,flag为false,当线程获取锁时,flag为true。
如果线程尝试获取锁,但flag为true, 线程就持续获取flag的状态。

获取flag的状态使用的是系统的CAS接口。
CAS接口一般是这样的,bool CAS(查询的flag,比较的期望值expect,想要设置的值desired);
如果flag和expect不一致,CAS接口返回false,可有用于继续循环,实现自旋。
如果flag和expect一直,把flag设置为desired,并返回true。

其他和锁相关的

读写锁

如果使用互斥锁,只要有一个线程获取锁,无论它对共享资源进行什么操作,别的线程都只能休眠等待。
读写锁,在互斥锁的基础上,区分了读、写操作。当一个线程获取了读取锁时,别的线程也可以获取读取锁;当一个线程获取了写入锁时,其余所有线程都休眠等待。
其中有一个问题就是,如果有线程在进行读操作,想要写入的线程怎么办?

读写锁是一种设计思路,网络上有比较多的示例。

递归锁

递归锁,std::recursive_mutex。
解决了一个线程需要重复获取一个锁,所产生的死锁问题。

使用递归锁,同一个线程,可以重复对一个资源加锁,只要最终释放锁的次数和加锁的次数一致就可以。

参考:
什么时候需要使用递归锁(递归mutex)? - Lion Long的回答 - 知乎

信号量

信号量,Semaphore。
信号量和信号(Signal)不同,信号量相当于多线程中一个共享资源的计数器。

信号量在创建时需要设置一个初始值,表示同时可以有几个任务(线程)可以访问某一块共享资源。
一个任务要想访问共享资源,前提是信号量大于0,当该任务成功获得资源后,将信号量的值减 1;
若当前信号量的值小于 0,表明无法获得信号量,该任务必须被挂起,等待信号量恢复为正值的那一刻;
当任务执行完之后,必须释放信号量,对应操作就是信号量的值加 1。
另外,对信号量的操作(加、减)都是原子的。互斥锁(Mutex)就是信号量初始值为 1 时的特殊情形,即同时只能有一个任务可以访问共享资源区。
来源:https://zhuanlan.zhihu.com/p/512969481

C++11 没有实现信号量(C++20实现的),可以通过系统API使用信号量,或者通过条件变量来模拟信号量(给条件变量增加一个线程计数判断)。

以下通过Windows API实现:

示例一:

#include <winbase.h>

int i = 0;
HANDLE sem_handle;
void _func()
{
    char msg[256] = { 0x00 };
     //等待信号量信号
    WaitForSingleObject(sem_handle, INFINITE);
    sprintf_s(msg, 256, "%s%d%s%d", " This is Th:" , std::this_thread::get_id() , " data:" , i);
    std::cout << msg << std::endl;
    //线程占据信号量后,信号量计数自动减1
     //释放时增加信号量计数
    ReleaseSemaphore(sem_handle, 1, NULL);
   
}

int main()
{
	//创建一个信号量,只能同时容纳2个线程
    sem_handle = CreateSemaphoreA(NULL, 2, 2, "SEMP"); 
    std::thread th1(_func);
    std::thread th2(_func);
    std::thread th3(_func);

    th1.join();
    th2.join();
    th3.join();
    CloseHandle(sem_handle);
}

输出:

 This is Th:21748 data:0 This is Th:24000 data:0

 This is Th:20240 data:0

线程21748 和 24000 获取了信号量,执行_func,所以没等到21748 的std::endl执行,他们就一起输出了,两个线程的输出连起来了。

线程20240等到前面线程release信号量之后,才获取信号量,所以单独一行输出。

示例二:

#include <winbase.h>

int i = 0;
HANDLE sem_handle;
void _func()
{
    char msg[256] = { 0x00 };
    
    WaitForSingleObject(sem_handle, INFINITE);
    sprintf_s(msg, 256, "%s%d%s%d", " This is Th:" , std::this_thread::get_id() , " data:" , i);
    std::cout << msg << std::endl;
    ReleaseSemaphore(sem_handle, 1, NULL);
}

int main()
{
	//创建一个信号量,只能同时容纳1个线程
    sem_handle = CreateSemaphoreA(NULL, 1, 2, "SEMP");
    std::thread th1(_func);
    std::thread th2(_func);
    std::thread th3(_func);

    th1.join();
    th2.join();
    th3.join();
    CloseHandle(sem_handle);
}

输出:

 This is Th:16128 data:0
 This is Th:21188 data:0
 This is Th:5260 data:0

创建了一个信号量,只能容纳一个线程,就相当于互斥锁了,所以3个线程每个线程都占一行输出。

参考:
WINAPI】CreateSemaphore_信号量

原子变量

C++ 提供了原子变量模板
std::atomic,可以把变量例如int、bool等,通过atomic模板作为原子变量使用。

死锁问题

死锁是一个线程获取了锁,但是没有解锁导致的,所有需要获取锁的进程都阻塞,而且无法自行解决的情况。

死锁大概有几种情况:

  1. 代码中加了锁,但是在return等处忘记解锁。
    ->可以用lock_guard 或者unique_lock,无需自行解锁
  2. 一个线程的函数调用中,不同函数都对一个互斥量加锁,会导致一个函数加锁后,调用另一个函数,被调用的函数中也要加锁,形成死锁。
    -> 使用递归锁,或者不是使用同一个互斥量加锁。
  3. 多线程,同时对多个互斥量加锁,例如一个线程先锁mutex1,再锁mutex2,另一个线程,先锁mutex2,再锁mutex1,两个线程形成死锁。
    -> 编程时应该避免这种情况,可以使用std::lock(mutex1, mutex2)同时加锁。

参考:
C++ 死锁及解决办法

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mfbz.cn/a/410466.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

常用实验室器皿耐硝酸盐酸进口PFA材质容量瓶螺纹盖密封效果好

PFA容量瓶规格参考&#xff1a;10ml、25ml、50ml、100ml、250ml、500ml、1000ml。 别名可溶性聚四氟乙烯容量瓶、特氟龙容量瓶。常用于ICP-MS、ICP-OES等痕量分析以及同位素分析等实验&#xff0c;也可在地质、电子化学品、半导体分析测试、疾控中心、制药厂、环境检测中心等机…

自定义神经网络三之梯度和损失函数激活函数

文章目录 前言梯度概述梯度下降算法梯度下降的过程 optimize优化器 梯度问题梯度消失梯度爆炸 损失函数常用的损失函数损失函数使用原则 激活函数激活函数和损失函数的区别激活函数Relu-隐藏层激活函数Sigmoid和Tanh-隐藏层Sigmoid函数Tanh&#xff08;双曲正切&#xff09; &l…

【Python从入门到进阶】49、当当网Scrapy项目实战(二)

接上篇《48、当当网Scrapy项目实战&#xff08;一&#xff09;》 上一篇我们正式开启了一个Scrapy爬虫项目的实战&#xff0c;对当当网进行剖析和抓取。本篇我们继续编写该当当网的项目&#xff0c;讲解刚刚编写的Spider与item之间的关系&#xff0c;以及如何使用item&#xff…

Excel工作表控件实现滚动按钮效果

实例需求&#xff1a;工作表中有多个Button控件&#xff08;工作表Form控件&#xff09;和一个ScrollBar控件&#xff08;工作表ActiveX控件&#xff0c;名称为ScrollBar2&#xff09;&#xff0c;需要实现如下图所示效果。点击ScrollBar控件实现按钮的滚动效果&#xff0c;实际…

Go的CSP并发模型实现M, P, G简介

GMP概念简介 G: goroutine&#xff08;协程&#xff0c;也叫用户态线程&#xff09; M: 工作线程(内核态线程) P: 上下文(也可以认为是cpu&#xff0c;逻辑cpu数量&#xff0c;可以在程序启动的时候设置这个数量&#xff0c;gomaxprocs函数设置) GMP 模型 在 Go 中&#xff…

黄金回收是去当铺还是金店?

黄金回收是指将闲置的黄金饰品或金条等物品出售或交换成现金或其他有价物。在选择回收渠道时&#xff0c;很多人会犹豫是去当铺还是金店。本文将探讨这两种回收方式的特点。 当铺是一种专门经营典当业务的场所&#xff0c;也提供黄金回收服务。通过当铺回收&#xff0c;您可以在…

【简写Mybatis】02-注册机的实现以及SqlSession处理

前言 注意&#xff1a; 学习源码一定一定不要太关注代码的编写&#xff0c;而是注意代码实现思想&#xff1a; 通过设问方式来体现代码中的思想&#xff1b;方法&#xff1a;5W1H 源代码&#xff1a;https://gitee.com/xbhog/mybatis-xbhog&#xff1b;https://github.com/xbh…

51单片机学习(5)-----蜂鸣器的介绍与使用

前言&#xff1a;感谢您的关注哦&#xff0c;我会持续更新编程相关知识&#xff0c;愿您在这里有所收获。如果有任何问题&#xff0c;欢迎沟通交流&#xff01;期待与您在学习编程的道路上共同进步。 目录 一. 蜂鸣器的介绍 1.蜂鸣器介绍 2.压电式蜂鸣器 &#xff08;无源…

生成式 AI - Diffusion 模型的数学原理(5)

来自 论文《 Denoising Diffusion Probabilistic Model》&#xff08;DDPM&#xff09; 论文链接&#xff1a; https://arxiv.org/abs/2006.11239 Hung-yi Lee 课件整理 讲到这里还没有解决的问题是&#xff0c;为什么这里还要多加一个噪声。Denoise模型算出来的是高斯分布的均…

NeurIPS 2023 Spotlight | VoxDet:基于3D体素表征学习的新颖实例检测器

本文提出基于3D体素表征学习的新颖实例检测器VoxDet。给定目标实例的多视图&#xff0c;VoxDet建立该实例的三维体素表征。在更加杂乱的测试图片上&#xff0c;VoxDet使用体素匹配算法检测目标实例。实验表明&#xff0c;VoxDet中的三维体素表征与匹配比多种二维特征与匹配要更…

【深入理解设计模式】适配器设计模式

适配器设计模式 适配器设计模式是一种结构型设计模式&#xff0c;用于将一个类的接口转换成客户端所期望的另一个接口&#xff0c;从而使得原本由于接口不兼容而不能一起工作的类能够一起工作。适配器模式通常用于以下场景&#xff1a; 现有接口与需求不匹配&#xff1a;当需要…

IP对讲终端SV-6002(防水)

SV-6002&#xff08;防水&#xff09;是一款IP对讲终端&#xff0c;具有10/100M以太网接口&#xff0c;其接收网络的音频数据&#xff0c;解码后播放&#xff0c;外部DC12~24V电源供电端子&#xff0c;提供单路2W的音频输出。基于TCP/IP网络通信协议和数字音频技术&#xff0c;…

【Java EE初阶二十三】servlet的简单理解

1. 初识servlet Servlet 是一个比较古老的编写网站的方式&#xff0c;早起Java 编写网站,主要使用 Servlet 的方式&#xff0c;后来 Java 中产生了一个Spring(一套框架)&#xff0c;Spring 又是针对 Servlet 进行了进一步封装,从而让我们编写网站变的更简单了&#xff1b;Sprin…

都有金蝶了,也能开发报表,为什么要用BI?

很多企业在一开始时都会有这样的困惑&#xff1a;我都有金蝶ERP了&#xff0c;也能自己开发报表&#xff0c;为什么还要买BI&#xff1f; 答案是显而易见的&#xff0c;金蝶ERP毕竟不是专业的数据分析系统&#xff0c;它的主要任务是在企业管理流程上&#xff0c;虽然很多企业…

Linux内核网络

文章目录 前言网络协议栈图解功能 发送Linux内核网络数据包图解流程 接收Linux内核网络数据包图解流程 最后 前言 你好&#xff0c;我是醉墨居士&#xff0c;因为Linux内核涉及的内容极多&#xff0c;我们初学者如果一上来就开始深挖细节&#xff0c;很有可能会在Linux内核代码…

MySQL - 事务日志

目录 1. redo日志 1.1 为什么需要REDO日志 1.2 REDO日志的好处、特点 1. 好处 2. 特点 1.3 redo的组成 1.4 redo的整体流程 1.5 redo log的刷盘策略 1.6 不同刷盘策略演示 1. 流程图 ​编辑2. 举例 1.7 写入redo log buffer 过程 1.8 redo log file 1. 相关参数…

[云原生] 二进制安装K8S(中)部署网络插件和DNS

书接上文&#xff0c;我们继续部署剩余的插件 一、K8s的CNI网络插件模式 2.1 k8s的三种网络模式 K8S 中 Pod 网络通信&#xff1a; &#xff08;1&#xff09;Pod 内容器与容器之间的通信 在同一个 Pod 内的容器&#xff08;Pod 内的容器是不会跨宿主机的&#xff09;共享…

批量解决opencv cv2.imread读取32位抠图png图像后,出现隐藏背景无法去除的问题

一、问题展示 1.原始png含蒙版抠图信息&#xff1a;位深度为32位&#xff0c;4通道图像信息&#xff0c;含蒙版背景信息 2.使用opencv读取保存后图像信息&#xff1a;位深度为24位&#xff0c;3通道图像信息&#xff0c;显示了扣除的背景 二、问题分析 1.用cv模块无法识别深度…

Stable Diffusion 绘画入门教程(webui)-ControlNet(线稿约束)

上篇文章介绍了openpose&#xff0c;本篇文章介绍下线稿约束&#xff0c;关于线稿约束有好几个处理器都属于此类型&#xff0c;但是有一些区别。 包含&#xff1a; 1、Canny(硬边缘&#xff09;&#xff1a;识别线条比较多比较细&#xff0c;一般用于更大程度得还原照片 2、ML…
最新文章