C++线程库(2)

news2024/11/15 15:46:06

C++线程库(2)

  • 线程同步
  • 互斥锁
  • 条件变量与互斥锁的搭配使用
    • 举例1
    • 举例2
    • 举例3

线程同步

在C++线程库(1)的博客中说了互斥量只能解决多个线程访问共享资源的问题,但是很明显没有次序感,而线程安全就是不同线程访问资源但是得到的结果是固定的就这就线程安全,所以为了保证线程安全,互斥锁一般与条件变量搭配使用。
我们知道进程是资源分配的基本单位,线程是CPU调度的基本单位。实际上我们创建线程就是在堆区申请了空间,而原本一个进程分配的4G空间中的堆区数据,代码区,数据区这些区域存放的数据都是资源,这些资源都可以被创建的线程进行访问。这就是线程进程的区别,线程可以共享资源,而多进程是拥有不同的资源。

void fun(int a,int b) {
	int c=a+b;
}
int main() {
	int x=10;
	int y=20;
	int z=0;
	z=fun(x,y);
}

如果我们编写程序这么一段程序,那么操作系统层面是怎么操作的呢?
在这里插入图片描述
首先在操作系统中存在很多寄存器,其中就有ebp和esp用于栈资源保护,比如上面代码中,主函数开始运行,在栈区从高地址向低地址进行分配内存,ebp首先在栈底,然后向上进行赋值,x=10,y=20,z=0,而调用x,y,z的时候呢就是通过ebp的偏移量进行调用,偏移4字节就是x,偏移8字节就是y,然后再栈顶重新分配内存,来传递参数,将x和y的值赋值给a和b,然后再向上有一块地址空间存放调用fun函数的后续地址,姐这上面会有一块地址空间存放主函数ebp现在的位置,然后ebp移动到该位置,开始调用该函数,如果访问c变量就向上偏移4字节,调用结束之后,会获取ebp的原本位置,进行复原,esp和ebp回到原本位置,执行其他代码。

互斥锁

void func(char ch) {
    for (int i = 0; i < 10; ++i) {
        for (int j = 0; j < 10; ++j) {
            printf("%c ", ch);
        }
        printf("\n");
    }
    printf("\n");
}

int main() {
    std::thread tha(func,'A');
    std::thread thb(func, 'B');
    std::thread thc(func, 'C');
    std::thread thd(func, 'D');
    std::thread the(func, 'E');
    tha.join();
    thb.join();
    thc.join();
    thd.join();
    the.join();
    return 0;
}

我们试运行上面代码,会发现其中打印出来是乱码,这是为什么呢?
因为我们的打印的输出都是再屏幕上,如果不同线程打印在不同的终端上就会出现不同的结果。
例如:

void func(char ch) {
    char filename[20] = {};
    sprintf(filename, "test%c.txt", ch);
    FILE* fp = fopen(filename, "w");
    for (int i = 0; i < 10; ++i) {
        for (int j = 0; j < 10; ++j) {
            fprintf(fp,"%c ", ch);
        }
        fprintf(fp,"\n");
    }
    fprintf(fp,"\n");
    fclose(fp);
    fp = nullptr;
}

这样我们就会创建不同的五个文件进行打印,我们运行之后也会发现创建了五个文件,其中的的数据也很整齐。
在这里插入图片描述

条件变量与互斥锁的搭配使用

举例1

std::mutex m_cv;
std::condition_variable cv;
std::string mydata;
bool ready=false;
bool processed = false;

void worker_thread() {
    std::unique_lock<std::mutex> lock(m_cv);
    while (!ready) {
        cv.wait(lock);
    }
    mydata += "数据处理完成";
    processed = true;
    lock.unlock();
    cv.notify_one();
}
int main() {
    std::thread tha(worker_thread);
    mydata += "data";
    {
        std::unique_lock < std::mutex >lock(m_cv);
        ready = true;
        cout << "main() signal data ready for processing" << endl;

    }
    cv.notify_one();
    {
        std::unique_lock<std::mutex> lock(m_cv);
        while (!processed) {
            cv.wait(lock);
        }
    }
    cout << "back int main(), data" << mydata << endl;
    tha.join();
    return 0;
}

我们先通过讲解上面代码进行理解:
首先程序编译链接,将程序编译为二进制文件(可执行文件),其实程序执行的第一个程序不是主函数,其实会有一个函数运行在主函数之前,用来初始化堆区,栈区,代码区等。
首先创建线程tha,然后主线程和tha线程两者竞争获得锁(用户态切换到内核态,获取锁,如果获得锁了就又切换到用户态,如果没有获得锁就会终端线程,就会将这个线程放入锁得等待队列中),我们举例tha线程获取到锁,他获取到锁之后呢ready为false,进入循环,进入cv等待函数。该函数有四个步骤,第一步:弃锁,第二步:终止线程,将线程放入条件变量得等待队列中,第三步:等待被唤醒,被唤醒之后线程会被放入互斥锁得等待队列中,第四步:获取锁。
而很明显这四个步骤中被卡在了第三步,此时呢因为其释放了锁,所以主线程可以获得锁了,获得锁之后将ready置为true,然后主线程执行了cv的唤醒函数,这样tha线程被唤醒,唤醒之后进入锁的等待队列,此时很明显两个线程又都在获取锁,我们举例这个时候主线程获取到锁,获取到锁之后,processed为false,所以进入循环,然后主线程也进入cv条件变量的等待队列,这里的流程和tha线程等待是一样的,也释放了锁资源,这个时候tha线程获得锁(从锁的等待队列出来),ready为true,继续执行,将processed置为true,然后唤醒主线程,主线程从条件变量的等待队列中出来,进入互斥锁的等待队列,因为在tha线程中工作函数结束之后资源会释放,所以函数结束,锁页会自动进行解锁,解锁之后主线程的会从互斥锁的等待队列中出来,最后因为processed为true,所以不会再次进入循环,执行下面语句,等待tha线程结束。而这个线程的瑕疵就在于在函数结束之后才会自动解锁,我们需要优化,在唤醒主线程前进行解锁。

举例2

同样先看代码:

const int n = 10;
int tag = 1;
std::mutex mtx;
std::condition_variable cv;
void funa() {
    std::unique_lock<std::mutex> lock(mtx);
    for (int i = 0; i < n; ++i) {
        while (tag!=1)
        {
            cv.wait(lock);
        }
        cout << "funa: A "<<endl;
        tag = 2;
        cv.notify_all();
    }
}
void funb() {
    std::unique_lock<std::mutex> lock(mtx);
    for (int i = 0; i < n; ++i) {
        while (tag != 2)
        {
            cv.wait(lock);
        }
        cout << "funb: B " << endl;
        tag = 3;
        cv.notify_all();
    }
}
void func() {
    std::unique_lock<std::mutex> lock(mtx);
    for (int i = 0; i < n; ++i) {
        while (tag != 3)
        {
            cv.wait(lock);
        }
        cout << "func: C " << endl;
        tag = 1;
        cv.notify_all();
    }
}
int main() {
    std::jthread tha(funa);
    std::jthread thb(funb);
    std::jthread thc(func);
    return 0;
}

运行结果

首先三个创建三个线程,三个线程在竞争锁,我们举例线程C获得锁,线程AB就在互斥锁的等待队列中,执行C线程,因为tag值为1,所以线程C进入条件变量的等待队列中,同样的四个步骤:

  • 释放锁
  • 进入条件变量的等待队列
  • 等待被唤醒,唤醒后进入锁的等待队列中
  • 获得锁
    也卡在了条件变量的等待队列中,然后线程AB中的第一获得锁,我们举例B获得锁,B线程和C线程一样,进入条件变量的等待队列中,然后A线程获得锁,不会进入条件变量的等待队列中,进行输出,然后将tag值设置为2,唤醒线程。注意这里是唤醒所有的线程,唤醒了BC线程,两个线程进入到锁的等待队列中,等待获取锁,而线程A会再一次进入循环而因为tag值设置为2,而进入条件变量的等待队列中,释放锁,BC线程抢占锁,如果C抢占到就会进入条件变量的等待队队列中,如果B抢到就会输出,从而唤醒其他线程,循环进入等待队列之后其他线程才可以获取锁,依次循环下去就可以按照顺序打印到想要的结果,因为tag的值和一个线程相对应,其他线程就算抢占到锁也会进入条件变量的等待队列中。
    唤醒所有线程:我们在讲解程序的时候大家也可以看到是唤醒所有线程,这个和唤醒一个有什么不一样吗?和名字一样,唤醒一个线程就会随机唤醒一个线程,而另一个就是唤醒所有的线程。这里为什么会用唤醒所有,而举例1会用唤醒一个呢?举例1只有两个线程,唤醒一个线程也肯定唤醒的是另一个线程,而这个程序创建了三个线程,如果线程A在输出之后唤醒了C线程,A线程调用完唤醒函数之后页会进入到条件变量的等待队列中,而C线程执行下去页会进入到条件变量的等待队列中,我们会发现程序锁死了,三个线程都在条件变量的等待队列中,没有程序来唤醒线程。----但是呢唤醒所有线程这里会存在一个惊群现象。惊群现象呢就是接收到数据之后唤醒了所有的线程,然而这些线程都不能处理这个数据而再一次进入条件变量等待队列,这很显然效率很低。所以编程过程中epoll一般只对应一个套接字,因为在epoll过滤接收到数据之后,只有相对应的套接字来处理。

举例3

我们试着编写一个程序,创建三个线程,调用funa,funb,func函数,依次输出1,2,3,4,5,6也就是线程a输出1,4,7等,线程b输出2,5,8,线程C输出3,6,9等输出到100
程序是这样编写的:

const int n = 100;
int tag = 1;
std::mutex mtx;
std::condition_variable cv;
void funa() {
    std::unique_lock<std::mutex> lock(mtx);
    while (tag <= n) {
        while (tag % 3 != 1 && tag <= n) {
            cv.wait(lock);
        }
        if (tag > n) break;
        if (tag % 3 == 1)
        cout << "funa:" << tag++ << endl;
        cv.notify_all();
    }
    cv.notify_all();
}
void funb() {
    std::unique_lock<std::mutex> lock(mtx);
    while (tag <= n) {
        while (tag % 3 != 2 && tag <= n) {
            cv.wait(lock);
        }
        if (tag > n) break;
        if(tag%3==2)
        cout << "funb:" << tag++ << endl;
        cv.notify_all();
    }
    cv.notify_all();
}
void func() {
    std::unique_lock<std::mutex> lock(mtx);
    while (tag <= n) {
        while (tag % 3 != 0 && tag <= n) {
            cv.wait(lock);
        }
        if (tag > n) break;
        if (tag % 3 == 0)
            cout << "func:" << tag++ << endl;
        cv.notify_all();
    }
     cv.notify_all();
}
int main() {
    std::jthread tha(funa);
    std::jthread thb(funb);
    std::jthread thc(func);
    return 0;
}

可以试着理解一下这段代码为什么这样编写?
为了防止在线程结束的时候有线程在条件变量的等待队列中,所以要唤醒,当然我们编写的这个程序因为在函数中设置的条件比较多所以也可在结束以不唤醒。
当然这个题可以有多种方法做出来,但是大家要有这样的习惯。

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

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

相关文章

最短路径算法-迪杰斯特拉(Dijkstra)算法(记录最短路径和距离)

原理&#xff1a; Dijkstra算法是解决**单源最短路径**问题的**贪心算法** 它先求出长度最短的一条路径&#xff0c;再参照该最短路径求出长度次短的一条路径 直到求出从源点到其他各个顶点的最短路径。 首先假定源点为u&#xff0c;顶点集合V被划分为两部分&#xff1a;集合…

chatgpt赋能python:Python字符串去除多余空格

Python字符串去除多余空格 随着Python在各个领域的应用越来越广泛&#xff0c;很多工程师都会遇到字符串去除多余空格的需求。而Python提供了简单的方法来解决这个问题&#xff0c;本文将详细介绍这些方法。 介绍 在Python中&#xff0c;字符串是很常见的数据类型&#xff0…

Linux环境下的工具(yum,gdb,vim)

一&#xff0c;yum yum其实是linux环境下的一种应用商店&#xff0c;主要用centos等版本。它也有三板斧&#xff1a;yum list,yum remove,yum install。当然不是说他只有这三个命令&#xff0c;还有yum search等等。在这直说以上三个。 yum list其实是查看你所能安装的软件包…

puppet 入门详解 超详细!!!

目录 一、puppet概述 二、Puppet的工作模式是什么&#xff1f; 三、Puppet的适用场景是什么&#xff1f; 四、原理 &#xff08;一&#xff09;工作模型 &#xff08;二&#xff09;工作流程 &#xff08;三&#xff09;使用模型 1、单机使用模型 2、master/agent 模型 &…

Vue中如何进行自动化部署与持续集成(CI/CD)

Vue中如何进行自动化部署与持续集成&#xff08;CI/CD&#xff09; 随着云计算和容器技术的广泛应用&#xff0c;自动化部署和持续集成&#xff08;CI/CD&#xff09;已经成为现代软件开发过程中必不可少的环节。Vue作为一款流行的前端框架&#xff0c;也可以使用自动化部署和…

解决:闹钟设置的自定义歌曲响铃时不会播放仅震动【Apple Music】【iOS】

文章目录 1、问题描述2、解决策略3、Q&A4、感受5、Tips 1、问题描述 自带铃声和震动脑瓜子嗡嗡的&#xff0c;幸好有apple music&#xff0c;在闹钟中可以轻松地选择你放入资料库中的任意一首音乐作为铃声。 奇怪的是&#xff0c;闹钟响起&#xff0c;仅震动&#xff0c;没…

chatgpt赋能python:Python怎么过滤非数字

Python怎么过滤非数字 在实际编程过程中&#xff0c;我们常常遇到要对一些数据进行处理&#xff0c;其中经常需要过滤掉非数字的数据&#xff0c;以保证程序能够正常运行。在Python中&#xff0c;若要过滤非数字&#xff0c;可以采用如下几种方法。 方法一&#xff1a;使用正…

chatgpt赋能python:Python中如何输入以0开头的数字?

Python中如何输入以0开头的数字&#xff1f; 在Python编程中&#xff0c;可能会遇到需要输入以0开头的数字的情况。然而&#xff0c;当我们尝试在Python shell或代码中输入以0开头的数字时&#xff0c;我们会发现Python会自动将其转换为八进制格式。 为什么Python会将以0开头…

使用MDK-ARM(KEIL V5)创建一个工程(有图有文字)

使用keil v5创建工程是一个比较复杂的过程&#xff0c;还希望读者能够耐下心来&#xff0c;过于浮躁会使创建过程出错&#xff0c;导致编译器无法编译等等许多问题。 言归正传&#xff0c;我们接下来开始说明创建过程&#xff0c;说明过程以图片为主&#xff0c;文字为辅&…

谷粒商城第四天-前端基础

目录 一、前言 二、学习的内容 一、ES6新语法 1.1 var与let 1.2 const 1.3 解构表达式的使用 1.4 字符串Api的使用 1.5 函数优化 1.6 箭头函数 1.7 对象优化 1.8 map和reduce 1.9 promise异步编排 1.10 模块化&#xff08;export和import的使用&#xff09;…

chatgpt赋能python:Python如何输出Pi

Python如何输出Pi Python是一门强大且易于学习的编程语言。它可以完成各种任务&#xff0c;包括数学计算和科学计算。在这篇文章中&#xff0c;我们将介绍如何使用Python输出圆周率Pi。 介绍 圆周率是一个重要的数学常数&#xff0c;用π表示。它代表了一个圆的周长与其直径…

树的前中后序遍历-非递归的迭代写法

就是要我们非递归其实就是模仿递归的写法&#xff0c;类如递归一样遍历一棵树&#xff0c;但是却不是递归的写法&#xff0c; 防止栈溢出。 二叉树的前序遍历 先看递归代码&#xff1a; void _preorderTraversal(TreeNode* root,vector<int>&v) {if (root NULL){…

C语言 结构体入门

目录 一、定义和使用结构体变量 1.1创建结构体类型 1.2定义结构体类型变量 1.先声明结构体类型&#xff0c;在定义该类型的变量 2.在声明类型的同时定义 1.3结构体成员的类型 1.4结构体变量的初始化和引用 1.5结构体的访问 二、结构体传参 前言&#xff1a;C语言提供…

直流稳压电源的几个性能指标

目录 电压调整率&#xff1a;输入电压在允许的范围内变化时&#xff0c;输出电压稳定性。 电流调整率&#xff1a;负载电流在允许的范围内变化时&#xff0c;输出电压稳定性 输出纹波电压&#xff1a;额定负载时&#xff0c;输出电压的振幅 电源效率&#xff1a;额定负载时&…

07-抚摸抽象边界:Golang 接口的多彩展现

&#x1f4c3;个人主页&#xff1a;个人主页 &#x1f525;系列专栏&#xff1a;Golang基础 &#x1f4ac;Go&#xff08;又称Golang&#xff09;是由Google开发的开源编程语言。它结合了静态类型的安全性和动态语言的灵活性&#xff0c;拥有高效的并发编程能力和简洁的语法。G…

Python 操作 Excel 全攻略 | 包括读取、写入、表格操作、图像输出和字体设置

文章目录 前言Python 操作 Excel 教程1. Excel 文件的读取与写入2. Excel 表格的操作2.1 插入和删除行和列2.2 遍历表格中的单元格并修改值 3. 图像的输出3.1 输出柱状图 4. 字体的设置4.1 设置单元格的字体大小和颜色4.2 设置单元格的加粗和斜体4.3 设置单元格的边框和填充颜色…

android实现无root获取其它应用data私有数据

实现原理就是反编译app的AndroidManifest文件&#xff0c;注意是反编译应用的资源文件&#xff0c;而不是编译整个app&#xff0c;这个操作不需要动应用的dex&#xff0c;难度上要容易得多。解码资源文件要用到一些工具&#xff0c;android下推荐ARSCLib。接下来是对目标应用重…

04_Linux设备树DTB文件OF函数

目录 创建小型模板设备树 添加cpus节点 添加soc节点 添加ocram节点 添加aips1、aips2和aips3这三个子节点 添加eespil、usbotg1和rngb这三个外设控制器节点 设备树在系统中的体现 根节点“/”各个属性 根节点“/”各子节点 特殊节点 aliases子节点 chosen子节点 L…

转专业之我见

写在前面 如果你点进来看这篇文章&#xff0c;说明你的至少有想转专业的想法甚至心里是趋向于转专业的。 但是或许是因为学校只有一次转专业的机会或者有别的原因让你犹豫不决&#xff0c;那么你首先要明确你为什么想要转专业&#xff0c;是因为本专业是天坑专业&#xff0c;…

UI 自动化测试 —— selenium的简单介绍和使用

selenium 是 web 应用中基于 UI 的自动化测试框架&#xff0c;支持多平台、多浏览器、多语言。 提到 UI 自动化就先了解什么是自动化测试&#xff1f; 目录 1. 自动化测试 2. UI 自动化 2.1 UI 自动化的特点 2.2 UI 自动化测试的优缺点 2.3 UI 自动化测试的使用对象 2.4…