【操作系统】多线程与多进程通信之深入理解【2023.01.31】

news2024/12/28 22:52:02

基本概念

首先,假设这么一个场景,进程A有线程Thread_A1和线程Thread_A2,进程B有线程Thread_B1和Thread_B2。举个例子,系统中进程的内存是独立的,也就是一台内存为4G的计算机,除了系统占用的1G部分,A进程可以申请到的内存是3G。B进程也可以申请到3G内存。可用内存只有3G,如何满足两个进程6G的要求?

实际上是通过映射的方式,A进程与B进程申请到的内存是虚拟内存,只有当进程被调度的时候才会真正映射到具体的物理内存。重点要清楚的是,进程A和进程B所申请的内存空间是独立的,也就是进程A里面0x6666FFFF所对应的数据和进程B里面0x6666FFFF地址所对应的数据是“八竿子打不着的”。

在这里插入图片描述
要知道在操作系统里面,进程是线程的容器,线程是执行任务的最小单元。一个进程里面最少有一个线程叫做主线程,否则单单一个容器是什么都做不了的。结合生活理解,比如一个小公司,肯定至少得有一个老板吧?至少也得有一个员工干活吧。如果没有员工的话,老板就是员工。这里把老板理解为进程员工理解为线程

通信分类

首先要对通信有个全面的理解,一般把传输(收、发)数据叫通信,而传输线程状态的信号叫同步。

(1)“同一个进程”的多个线程进行通信

在这里插入图片描述

一个老板可以有多个员工。多个员工要“可靠”通信,比如说两个员工A1和员工A2要讨论项目的方案,怎么办?很简单,公司都有文件柜,通过文件的形式,同一个公司的员工A1与A2可以访问文件柜里的文件,两个员工的通信通过文件的形式就可以了。

对应到程序里面,因为进程是线程的容器,一个进程的多个线程都是在一片内存区里面,比如一个线程在堆上的申请的内存,存放变量global_Var,地址是0x8888FFFF,该进程中的其它线程通过该地址访问到的是同一个变量global_Var。

😙这样的话线程通信就很简单了啊,我只要有变量的指针就可以访问了啊。代码如下:

#include <QCoreApplication>
#include <iostream>
#include <thread>
#include <QDateTime>
#include <QDebug>
void fun(char* out,const char * caller)
{
    using namespace std;
    cout << caller << out;
}
int main(int argc, char *argv[])
{
    using namespace std;
    QCoreApplication a(argc, argv);
    char * out = "Hello My baby!\n";

    thread t1(fun,out,"Thread1:");
    thread t2(fun,out,"Thread2:");
    t1.join();
	t2.join();
    return a.exec();
}

程序输出:
Thread1:Hello My baby!
Thread2:Hello My baby!

上面的例子仅仅说明了同一个进程下的多个线程可以通过一个指向堆内存的指针进行访问(事实上栈区内存也可以)。

😤比如我们把刚刚的线程数量增加一些,代码改成下面这样子:

#include <QCoreApplication>
#include <iostream>
#include <thread>
#include <QDateTime>
#include <QDebug>


void fun(char* out,const char * caller)
{
    using namespace std;
    cout << caller << out;
    if(0 == strcmp(caller,"Thread1:"))//**我们对线程1进行sleep操作,让其挂起一会
    {
        std::this_thread::sleep_for(std::chrono::seconds(5));
    }
}

int main(int argc, char *argv[])
{
    using namespace std;
    QCoreApplication a(argc, argv);
    char * out = "Hello My baby!\n";

    thread t1(fun,out,"Thread1:");
    thread t2(fun,out,"Thread2:");
    thread t3(fun,out,"Thread3:");
    thread t4(fun,out,"Thread4:");
    thread t5(fun,out,"Thread5:");

    qDebug() << QDateTime::currentDateTime().toString();
    t1.join();
    qDebug() << QDateTime::currentDateTime().toString();
    t2.join();
    t3.join();
    t4.join();
    t5.join();
    return a.exec();
}

程序输出:
在这里插入图片描述

现象分析:多核CPU下的多线程程序是可以并发执行的,上面的结果产生的原因是:cout到控制台的“打印”资源是唯一的,也就是多个线程都往控制台打印,如果不对资源进行保护的话,就存在线程1还没打印完,线程2又开始打印了。

多线程并发执行时互斥资源的同步问题

什么是同步呢?
答:同步是指:对于多线程程序,在一定的时间内只允许某一个线程来访问某个资源。而在此时间内,不允许其他的线程访问该资源。

比如说两个员工A1和员工A2,两人要共用一个打印机打印文档。如果没有保护机制,A1正在打印过程中,A2强行打印他的文档…岂不是要打起来喽。

实际上是这样处理的,公司把打印机放到一个简易的房子里,留了一个锁孔。
在这里插入图片描述
A员工要用打印机就锁住打印机的门,等到打印完了再把锁撤销掉。A正在打印的时候B员工就加不了锁了,从而避免了冲突。

对应到程序里面,Linux系统下使用互斥锁(Mutex)、条件变量(condition variable)、读写锁(reader-writer lock)、信号量(semaphore)都可以用来同步资源。Windows系统下可以使用临界区进行同步。

1️⃣临界区

一个进程里的多个线程进行同步,Windows下用的最多的“锁”是临界区,window系统支持,Linux不支持(对此的理解是Linux进程比Windows的进程机制不同)。🚩注意:临界区只能用于对象在同一进程里线程间的互斥访问;也就是临界区只能用于保证同一个公司的不同员工互斥的访问。还是对上面的代码进行改造。代码如下:

#include <QCoreApplication>
#include <iostream>
#include <thread>
#include <QDateTime>
#include <QDebug>
#include <windows.h>

using namespace std;

CRITICAL_SECTION cs;//1.定义临界区对象

void fun(char* out,const char * caller)
{
    EnterCriticalSection(&cs);//{{ 进入临界区 本临界区保护的是唯一的cout资源
    cout << caller << out;
    LeaveCriticalSection(&cs);//{{ 离开临界区
    if(0 == strcmp(caller,"Thread1:"))//**我们对线程1进行sleep操作,让其挂起一会
    {
        std::this_thread::sleep_for(std::chrono::seconds(5));
    }
}

int main(int argc, char *argv[])
{
    QCoreApplication a(argc, argv);
    InitializeCriticalSection(&cs);//2.初始化临界区

    char * out = "Hello My baby!\n";

    thread t1(fun,out,"Thread1:");
    thread t2(fun,out,"Thread2:");
    thread t3(fun,out,"Thread3:");
    thread t4(fun,out,"Thread4:");
    thread t5(fun,out,"Thread5:");

    qDebug() << QDateTime::currentDateTime().toString();
    t1.join();
    qDebug() << QDateTime::currentDateTime().toString();
    t2.join();
    t3.join();
    t4.join();
    t5.join();

    DeleteCriticalSection(&cs);//5.删除临界区
    return a.exec();
}

2️⃣互斥锁

互斥锁也很简单,创建了互斥锁对象之后进行加锁解锁就行了。与临界区的不同之处是:多个进程的线程间也可以用互斥锁进行同步。其实也不难理解,互斥锁mutex是内核对象,是系统内核进行管理的,而临界区是为了提高性能实现的一个用户态的对象。代码如下:

#include <QCoreApplication>
#include <iostream>
#include <thread>
#include <QDateTime>
#include <QDebug>
#include <windows.h>
#include <mutex>

using namespace std;

mutex g_mtx;

void fun(char* out,const char * caller)
{
    g_mtx.lock();// 1.加锁
    cout << caller << out;
    g_mtx.unlock();// 2.释放锁
    if(0 == strcmp(caller,"Thread1:"))//**我们对线程1进行sleep操作,让其挂起一会
    {
        std::this_thread::sleep_for(std::chrono::seconds(5));
    }
}

int main(int argc, char *argv[])
{
    QCoreApplication a(argc, argv);
    char * out = "Hello My baby!\n";

    thread t1(fun,out,"Thread1:");
    thread t2(fun,out,"Thread2:");
    thread t3(fun,out,"Thread3:");
    thread t4(fun,out,"Thread4:");
    thread t5(fun,out,"Thread5:");

    qDebug() << QDateTime::currentDateTime().toString();
    t1.join();
    qDebug() << QDateTime::currentDateTime().toString();
    t2.join();
    t3.join();
    t4.join();
    t5.join();

    return a.exec();
}

3️⃣条件变量

条件变量的主要使用场景是生产者消费者模型,条件变量是多线程程序中用来实现等待和唤醒常用的方法。
举例:妈妈做好饭了,需要通知所有家人吃饭,家人们也要知道妈妈是否做好饭了,不能一直问“妈,饭好了吗”、“老婆,饭好了吗?”…。而应该等待妈妈通知,“饭做好了”,这里的条件变量就是表征饭是否做好了。

程序中,c++标准库中条件变量condition_variable是要用到unique_lock,unique_lock依赖构造和析构机制进行自动的mutexlockunlock操作。先看❌错误代码:

#include <QCoreApplication>
#include <iostream>
#include <thread>
#include <QDateTime>
#include <QDebug>
#include <windows.h>
#include <mutex>
#include <condition_variable>
using namespace std;
mutex g_mtx;//互斥量
condition_variable g_cond;//条件变量

void fun(char* out,const char * caller)
{
    unique_lock<mutex> ulck(g_mtx);//这里需要了解下独占锁,
    g_cond.wait(ulck); //wait函数会阻塞等待信号,同时会unlock g_mtx对象,让其他线程可以通过g_mtx进行加锁;当wait有信号后会对g_mtx进行加锁。

    cout << caller << out;

    if(0 == strcmp(caller,"Thread1:"))//**我们对线程1进行sleep操作,让其挂起一会
    {
        std::this_thread::sleep_for(std::chrono::seconds(5));
    }
}

int main(int argc, char *argv[])
{
    QCoreApplication a(argc, argv);
    char * out = "Hello My baby!\n";

    thread t1(fun,out,"Thread1:");
    thread t2(fun,out,"Thread2:");
    thread t3(fun,out,"Thread3:");
    thread t4(fun,out,"Thread4:");
    thread t5(fun,out,"Thread5:");

    //g_cond.notify_one(); 这样的话只会唤醒一个线程,
    g_cond.notify_all();//唤醒所有通过wait阻塞的线程。
    t1.join();
    t2.join();
    t3.join();
    t4.join();
    t5.join();

    return a.exec();
}

程序结果如下图:
在这里插入图片描述

这是什么鬼👻?

⭐️有两种可能的原因:
🖕情况1:线程notify发出通知消息的时,线程还未执行,导致通知消息被丢失。
🖕情况2:线程的虚假唤醒,也就是操作系统会把阻塞的线程进行“错误”唤醒,具体机制很复杂,暂时知道有这个bug就行。如果线程被虚假唤醒则对输出到控制台的资源就失去了保护。
线程的几种状态,图片来自网络
正确的代码如下:

#include <QCoreApplication>
#include <iostream>
#include <thread>
#include <QDateTime>
#include <QDebug>
#include <windows.h>
#include <mutex>
#include <condition_variable>

using namespace std;

bool g_flag = false;//注意这里,全局变量多线程访问,为啥不用加锁?线程安全吗?这是因为本例只有一个线程对其写值。

mutex g_mtx;//互斥量
condition_variable g_cond;//条件变量

void fun(char* out,const char * caller)
{
    unique_lock<mutex> ulck(g_mtx);//这里需要了解下独占锁,
    g_cond.wait(ulck,[](){  return g_flag; }); //wait函数会阻塞等待信号,同时会unlock g_mtx对象,让其他线程可以通过g_mtx进行加锁;当wait有信号后会对g_mtx进行加锁。

    cout << caller << out;

    if(0 == strcmp(caller,"Thread1:"))//**我们对线程1进行sleep操作,让其挂起一会
    {
        std::this_thread::sleep_for(std::chrono::seconds(5));
    }
}

int main(int argc, char *argv[])
{
    QCoreApplication a(argc, argv);
    char * out = "Hello My baby!\n";

    thread t1(fun,out,"Thread1:");
    thread t2(fun,out,"Thread2:");
    thread t3(fun,out,"Thread3:");
    thread t4(fun,out,"Thread4:");
    thread t5(fun,out,"Thread5:");

    {
        unique_lock<mutex> ulck(g_mtx);//仅对花括号里面的两条语句进行加锁
        g_flag = true;//通过标记位1、防止线程虚假唤醒,2、防止信号丢失(lambda表达式会在while循环里不断执行。)
        g_cond.notify_all();//唤醒所有通过wait阻塞的线程。
    }

    //qDebug() << QDateTime::currentDateTime().toString();
    t1.join();
    //qDebug() << QDateTime::currentDateTime().toString();
    t2.join();
    t3.join();
    t4.join();
    t5.join();

    return a.exec();
}

4️⃣读写锁

临界区、互斥锁、条件变量所保护的资源一次只能被一个线程所使用。但是对于一些变量我们读的次数远大于写的次数,而读又不会导致冲突。杨超越一次只能有一个老公,但是粉丝可以有无数的呀,粉丝只是欣赏美貌,看看而已。
读写锁,就是这么来滴。如果是读操作的话是可以重复加锁成功的,但是只要有锁(不管读锁还是写锁)加成功了,写锁就加不进去了。

👧程序加读锁的小例子

// testSharedLock.cpp : 此文件包含 "main" 函数。程序执行将在此处开始并结束。
//

#include <iostream>
#include <shared_mutex>
#include <thread>
#include <string>
#include <stdlib.h>

using namespace std;

string g_RealBody("***杨超越本尊***");



void fun(const char* ch)
{
    shared_lock<shared_mutex> slck;
    string str("start ");
    
    str += "\"---> ";
    str += string(ch);
    str += g_RealBody;// 相当于对全局变量 加 读 锁
    str += " \"";
    system(str.c_str());
}


int main()
{

    thread t1(fun,"Fans1:Take a photo! ");
    thread t2(fun, "Fans2:Take a photo! ");
    thread t3(fun, "Fans3:Take a photo! ");
    thread t4(fun, "Fans4:Take a photo! ");


    t1.join(); t2.join(); t3.join(); t4.join();
    std::cout << "Hello World!\n";
}


程序执行结果如下图,注意看窗口名字,我四个粉丝共享读锁并发进行拍照,需自己脑补一下。
在这里插入图片描述
👫程序加写锁的例子:

// testSharedLock.cpp : 此文件包含 "main" 函数。程序执行将在此处开始并结束。
//

#include <iostream>
#include <shared_mutex>
#include <thread>
#include <string>
#include <stdlib.h>
#include <windows.h>
using namespace std;

string g_RealBody("***杨超越本尊***");

shared_mutex g_mtx;

void fun(const char* ch)
{
    shared_lock<shared_mutex> slck(g_mtx);
    string str("start ");
    
    str += "\"---> ";
    str += string(ch);
    str += g_RealBody;// 相当于对全局变量 加 读 锁
    str += " \"";
    system(str.c_str());
}


int main()
{
    thread t0( [](){
        unique_lock<shared_mutex> ulck(g_mtx);//独占锁 占住不释放,其他读锁也不能进行
        while (1)
        {
            cout << "超越正在度蜜月,粉丝们 No 拍照!\n";
            Sleep(100);
        }
    });
    thread t1(fun,"Fans1:Take a photo! ");
    thread t2(fun, "Fans2:Take a photo! ");
    thread t3(fun, "Fans3:Take a photo! ");
    thread t4(fun, "Fans4:Take a photo! ");


    t0.join(); t1.join(); t2.join(); t3.join(); t4.join();
    std::cout << "Hello World!\n";
}

程序执行结果如下,写锁占住超越之后,具有独占性,导致其他读锁无法访问。
在这里插入图片描述

5️⃣信号量

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Oracle——分析函数

文章目录前言介绍demo案例测试测试前的准备rank()dense_rank()row_number()前言 分析函数在oracle中比较常见&#xff0c;但用的不多&#xff0c;今天有幸碰见&#xff0c;索性做一次使用的总结说明。 介绍 oracle中的分析函数&#xff0c;常见的有下面的三种&#xff1a; r…

四季度亏损扩大,Meta Reality Labs近4年财报营收汇总

2月2日青亭网报道&#xff0c;Meta今天发布了2022财年第四季度财报、全年财报。根据22年四季度财报信息显示&#xff0c;Reality Labs业务部门营收营收7.27亿美元&#xff0c;同比&#xff08;21年四季度8.77亿美元&#xff09;要下降17.1%&#xff1b;净亏损42.79亿美元&#…

R语言多元数据统计分析在生态环境中的应用

生态环境领域研究中常常面对众多的不同类型的数据或变量&#xff0c;当要同时分析多个因变量&#xff08;y&#xff09;时需要用到多元统计分析&#xff08;multivariate statistical analysis&#xff09;。多元统计分析内容丰富&#xff0c;应用广泛&#xff0c;是非常重要和…

关于java中的BigInteger

文章目录关于BigInteger类BigInteger的构造方法BigInteger方法使用示例算术运算API转换为基本数据类型的方法小结关于BigInteger类 BigInteger类是用于解决整型类型&#xff08;含基本数据类型及对应的包装类&#xff09;无法表示特别大的数组及运算问题。 即使是占用字节数最…

HTB_Weak RSA

文章目录信息收集RSA 算法题目解密信息收集 下载&#xff0c;解压 (密码都是hackthebox) enc后缀的文件是一种加密文件&#xff0c;打开为乱码&#xff0c;key.pub内容如下&#xff0c;是 RSA 加密算法的公钥 -----BEGIN PUBLIC KEY----- MIIBHzANBgkqhkiG9w0BAQEFAAOCAQwAMI…

CISP信息安全认证考试都考什么?

CISP考试是目前热门的信息安全认证考试&#xff0c;很多刚刚开始了解CISP的朋友&#xff0c;比较关心关于CISP考试内容的相关问题&#xff0c;今天就由中培小编带大家一起去看看CISP认证考试究竟都考哪些内容&#xff1f;首先来看一下试卷结构考试时间&#xff1a;120分钟考试题…

CSS图标与链接

目录 如何添加图标 Font Awesome 图标 实例 Bootstrap 图标 实例 Google 图标 实例 为图标添加样式或颜色 设置链接样式 实例 实例 文本装饰 实例 背景色 实例 链接按钮 实例 更多实例 如何添加图标 向 HTML 页面添加图标的最简单方法是使用图标库&#xff0…

【Less】全局样式重复注入问题

// package.json {"less": "^4.1.3","vite": "^3.1.0", }参考&#xff1a; [less/sass]如何避免因公共模块导致生成重复css代码解决 Vue CSS 样式重复载入&#xff0c;为 Vue 添加全局 less 或 sass 基础样式库【不是本篇解决方法&am…

vue3学习笔记之router(router4 + ts)

文章目录Vue Router1. 基本使用router-view2. 路由跳转2.1 router-link2.2 编程式导航2.3 replace3. 路由传参4. 嵌套路由5. 命令视图6. 重定向和别名6.1 重定向6.2 别名7. 路由守卫7.1 全局前置守卫7.2 全局后置守卫案例&#xff1a;加载滚动条8. 路由元信息9. 路由过渡动效10…

瞧不上alert 老古董?使用alert实现一个精美的弹窗

曾几何时alert陪伴了我很多歌日日夜夜&#xff0c;但现在人们越来越追求高端的技术&#xff0c;其实慢慢的我也都快淡忘了前端的世界里还有alert这么一个伟大的成员。 目录 一、为什么抛弃了alert? 1. 不同浏览器的表现 2. 第三方组件的使用 3. 代码意识的控制 二、用al…

2023年浏览器哪个好用速度快,看这一篇就够了

在网络覆盖的社会&#xff0c;不管走到哪里&#xff0c;都能上网浏览新闻、看热点资讯。浏览器是用户上网浏览的必要软件之一&#xff0c;它决定这用户浏览网页的速度和习惯。那么&#xff0c;2023年什么浏览器好用稳定速度快&#xff1f;目前优秀的浏览器有很多&#xff0c;但…

HDFS常用命令汇总

HDFS常用命令汇总一、前言信息二、帮助信息查看1、查看帮助信息2、帮助文档&#xff08;附带命令的详细说明&#xff09;三、常用命令1、创建目录2、查看目录下的内容3、上传文件4、上传并删除源文件5、查看文件内容6、查看文件开头内容7、查看文件末尾内容8、下载文件9、合并下…

ceph中报错“ clock skew detected on mon.ceph2, mon.ceph3”

自己搭建的ceph集群&#xff0c;显示时间不同步:clock skew detected on mon.ceph2, mon.ceph3但是查看chrony进程已经启动&#xff0c;ceph配置文件中,如下参数也已经配置&#xff0c;查看chrony.conf配置文件发现&#xff0c;同步源没有修改过&#xff0c;默认的于是修改ceph…

面试官的几句话,差点让我挂在HTTPS上

面试官的几句话&#xff0c;差点让我挂在HTTPS上 目录&#xff1a;导读 一、HTTP 协议 二、HTTPS 协议 三、使用 HTTP 协议还是 HTTPS 协议呢&#xff1f; 四、HTTP 协议和 HTTPS 协议的区别 作为软件测试&#xff0c;大家都知道一些常用的网络协议是我们必须要了解和掌握…

MySQL jdbc 反序列化分析

0x01 前言听师傅们说这条链子用的比较广泛&#xff0c;所以最近学一学&#xff0c;本来是想配合着 tabby 或是 codeql 一起看的&#xff0c;但是 tabby 的环境搭建一直有问题&#xff0c;耽误了很久时间&#xff0c;所以就直接看了0x02 JDBC 的基础• 本来不太想写这点基础的&a…

敏捷与DevOps的区别,知异同,发准力

​图片来自Robert Martin《敏捷整洁之道》。敏捷DevOps生命之环&#xff0c;由内到外&#xff0c;第一圈&#xff1a;Kent Beck&#xff0c;第二圈&#xff1a;Uncle Bob&#xff0c;第三圈&#xff1a;Mike Cohn。三圈由内到外分别是&#xff1a;编程、工程&#xff08;大致对…

STM32读取24位模数转换(24bit ADC)芯片TM7711数据

STM32读取24位模数转换&#xff08;24bit ADC&#xff09;芯片TM7711数据 TM7711是一款国产低成本24位ADC芯片&#xff0c;常用于与称重传感器配合实现体重计的应用。这里介绍STM32读取TM7711的电路和代码实现。TM7711与HX710A是兼容的芯片&#xff0c;而与HX711在功能上有所不…

B+树的概念

与分块查找和B树类似。 一棵m阶的B树需满足如下条件: 每个分支结点最多有m棵子树非叶子结点的根结点至少有两棵子树&#xff0c;其他结点至少有⌈m/2⌉\lceil m/2\rceil⌈m/2⌉棵子树结点的子树个数与关键字个数相等关键字全部存储在叶子结点中。所有指向对应记录的指针也存储…

LeetCode刷题---链表经典问题(双指针)

文章目录一、编程题&#xff1a;206. 反转链表&#xff08;双指针-头插法&#xff09;解题思路1.思路2.复杂度分析&#xff1a;3.算法图解代码实现二、编程题&#xff1a;203. 移除链表元素解题思路1.思路2.复杂度分析&#xff1a;3.算法图解代码实现三、编程题&#xff1a;328…

嵌入式开发:为什么物联网正在吞噬嵌入式操作系统?

在过去几年的嵌入式开发中&#xff0c;独立嵌入式软件市场的两大基石已被物联网公司完全吞噬。第一个FreeRTOS被亚马逊吞并&#xff0c;以支持其亚马逊Web服务(AWS)云平台的物联网开发&#xff0c;Express Logic被微软吞并&#xff0c;用于其竞争对手Azure云服务。许多分析师对…