《C++11:通过thread类编写C++多线程程序》

关于多线程的概念与理解,可以先了解Linux下的底层线程。当对底层线程有了一定程度理解以后,再学习语言级别的多线程编程就轻而易举了。

【Linux】多线程 -> 从线程概念到线程控制

【Linux】多线程 -> 线程互斥与死锁


语言级别的多线程编程最大的好处就是可以跨平台,使用语言级别编写的多线程程序不仅可以在Windows下直接编译运行,在Linux下也是可以直接编译运行的。其实本质还是调用了不同操作系统的底层线程API。

多线程简单使用

  • 怎么创建启动一个线程?

#include<iostream>
#include<thread>
#include<chrono>void threadHandle1(int time)
{// 让线程休眠两秒std::this_thread::sleep_for(std::chrono::seconds(time));std::cout << "hello thread1" << std::endl;
}int main()
{// 创建一个线程对象,传入线程函数以及线程函数所需要的参数,线程就开始执行了std::thread t1(threadHandle1, 2);// 主线程等待子线程结束,再继续往下运行t1.join();std::cout << "main thread done!" << std::endl;return 0;
}

主线程要t1.join()等待子线程结束之后才能向后运行。如果不等待,就会异常终止。

  • 子线程如何结束?

子线程函数运行完成就结束了。

也可以将线程设置为分离状态,主线程就可以不用等待子进程执行完再继续向后运行了。

void threadHandle1(int time)
{// 让线程休眠两秒std::this_thread::sleep_for(std::chrono::seconds(time));std::cout << "hello thread1" << std::endl;
}int main()
{// 创建一个线程对象,传入线程函数以及线程函数所需要的参数,线程就开始执行了std::thread t1(threadHandle1, 2);// 主线程等待子线程结束,再继续往下运行// t1.join();// 设置线程为分离状态t1.detach();std::cout << "main thread done!" << std::endl;return 0;
}

  •  主线程如何处理子线程?

1、t1.join()等待线程,等待线程结束后主线程再继续向后运行。

2、t1.detach()分离线程,主线程结束,整个进程就结束了,所有线程都自动结束了。


#include<iostream>
#include<thread>
#include<chrono>void threadHandle1(int time)
{// 让线程休眠time秒std::this_thread::sleep_for(std::chrono::seconds(time));std::cout << "hello thread1" << std::endl;
}void threadHandle2(int time)
{// 让线程休眠time秒std::this_thread::sleep_for(std::chrono::seconds(time));std::cout << "hello thread1" << std::endl;
}int main()
{// 创建一个线程对象,传入线程函数以及线程函数所需要的参数,线程就开始执行了std::thread t1(threadHandle1, 2);std::thread t2(threadHandle2, 2);// 主线程等待子线程结束,再继续往下运行t1.join();t2.join();// 设置线程为分离状态// t1.detach();std::cout << "main thread done!" << std::endl;return 0;
}

 mutex互斥锁和lock_guard

#include<iostream>
#include<thread>
#include<list>int tickets = 1000;void getTicket(int i)
{// 模拟用户抢票的行为while (tickets > 0){std::cout << "用户:" << i << "正在进行抢票!" << tickets << std::endl;tickets--;std::this_thread::sleep_for(std::chrono::microseconds(100));}
}int main()
{std::list<std::thread> list;for (int i = 1; i <= 3; i++){list.push_back(std::thread(getTicket, i));}for (std::thread &t: list){t.join();}return 0;
}

输出的结果杂乱无章,并且抢票出现负数。

多线程对临界区的访问会存在竞态条件:临界区代码段在多线程环境下执行,随着线程的调度时许不同,从而产生不同的结果。所以,我们要保证对临界区的原子操作,是通过对临界区加锁完成的。

由于多个线程同时向标准输出流std::cout输出信息,会造成输出混乱,各线程的输出可能相互穿插。

在多线程环境下,多个线程同时访问和修改共享资源tickets,会引发数据竞争问题。比如,当一个线程检查到tickets>0后,在执行tickets--操作之前,另一个线程也可能检查到tickets>0,进而导致重复售票或者出现负数票数的情况。

++/--操作并不是原子性的,其实是对应三条汇编指令完成的。

  • 读取:从内存中把变量的值读取到寄存器
  • 修改:在寄存器里将变量的值+1/-1
  • 写入:把修改后的值写入到内存

在单线程环境下,这三个步骤顺序执行不会有问题。但是在多线程环境下,多个线程可能对同一个变量同时进行++/--操作,从而导致数据竞争的问题。

可以看下面的过程演示。

一:

二:

三:

C++11是通过加锁来保证++/--操作的原子性的。


#include<iostream>
#include<thread>
#include<list>
#include<mutex>int tickets = 1000;
std::mutex mtx; // 全局的一把锁void getTicket(int i)
{// 模拟用户抢票的行为while (tickets > 0){mtx.lock();// 锁+双重判断,如果不双重判断,当tickets为1时,用户正在抢票还没有执行到tickets--,其他用户判断tickets>0,也进来了等待锁。// 用户抢票之后,其他用户获取到锁还会进行抢票。if (tickets > 0){// 临界区代码段 - 加锁保护std::cout << "用户:" << i << "正在进行抢票!" << tickets << std::endl;tickets--;mtx.unlock();}}
}int main()
{std::list<std::thread> list;for (int i = 1; i <= 3; i++){list.push_back(std::thread(getTicket, i));}for (std::thread &t: list){t.join();}return 0;
}

通过加锁和双重判断的方式,这样就能做到对多线程对共享资源tickets的安全访问了。

lock_guard和unique_lock

其实不需要我们手动加锁和解锁,因为,如果临界区内有return,break等语句,此线程获取锁,但是在释放锁之前break或者renturn了,导致锁没有释放。那么其他线程也获取不了锁,就会造成死锁问题,所以对于这把互斥锁要有RAII的思想。

使用lock_guard。构造函数获取锁,析构函数释放锁。

int tickets = 1000;
std::mutex mtx; // 全局的一把锁void getTicket(int i)
{// 模拟用户抢票的行为while (tickets > 0){{std::lock_guard<std::mutex> lock(mtx);// mtx.lock();// 锁+双重判断,如果不双重判断,当tickets为1时,用户正在抢票还没有执行到tickets--,其他用户判断tickets>0,也进来了等待锁。// 用户抢票之后,其他用户获取到锁还会进行抢票。if (tickets > 0){std::cout << "用户:" << i << "正在进行抢票!" << tickets << std::endl;tickets--;}// mtx.unlock();}std::this_thread::sleep_for(std::chrono::microseconds(100));}
}

不管临界区是正常执行,还是break或return了,出了作用域,lock_guard会自动调用析构函数释放锁,保证所有线程都能释放锁,避免死锁问题发生。

lock_guard不支持拷贝构造和赋值运算符重载,如果需要拷贝和赋值可以使用unique_lock,支持移动语义,可以使用右值引用的拷贝构造和赋值运算符重载。

int tickets = 1000;
std::mutex mtx; // 全局的一把锁void getTicket(int i)
{// 模拟用户抢票的行为while (tickets > 0){{std::unique_lock<std::mutex> lck(mtx);// std::lock_guard<std::mutex> lock(mtx);// mtx.lock();// 锁+双重判断,如果不双重判断,当tickets为1时,用户正在抢票还没有执行到tickets--,其他用户判断tickets>0,也进来了等待锁。// 用户抢票之后,其他用户获取到锁还会进行抢票。if (tickets > 0){std::cout << "用户:" << i << "正在进行抢票!" << tickets << std::endl;tickets--;}// mtx.unlock();}std::this_thread::sleep_for(std::chrono::microseconds(100));}
}

总结:lock_guard不能用在函数传递或返回的过程中,因为lock_guard删除了拷贝构造和赋值,只能用在简单的临界区代码段的互斥操作中。unique_guard可以用在函数传递或返回的过程中,因为支持移动语义,可以使用移动构造和移动赋值。unique_guard通常与条件变量一起使用。

线程间的同步通信

生产者-消费者模型

std::mutex mtx;
std::condition_variable cv;// 边生产边消费
class Queue
{
public:void put(int val){// 加锁std::unique_lock<std::mutex> lock(mtx);// 如果队列不为空,则等待while (!que.empty()){// 1.进入等待状态 2.释放锁cv.wait(lock);// 被唤醒之后,等待状态进入阻塞状态,获取锁进入就绪状态继续执行}que.push(val);cv.notify_all(); // 通知消费者消费std::cout << "生产者 生产:" << val << "号物品" << std::endl;}int get(){// 加锁std::unique_lock<std::mutex> lock(mtx);// 如果队列为空,则等待while (que.empty()){// 1.进入等待状态 2.释放锁cv.wait(lock);// 被唤醒之后,等待状态进入阻塞状态,获取锁进入就绪状态继续执行}int val = que.front();que.pop();cv.notify_all(); // 通知生产者生产std::cout << "消费者 消费:" << val << "号物品" << std::endl;return val;}
private:std::queue<int> que;
};// 生产
void producer(Queue* que)
{for (int i = 0; i <= 10; i++){que->put(i);std::this_thread::sleep_for(std::chrono::microseconds(100));}
}
// 消费
void conducer(Queue* que)
{for (int i = 0; i <= 10; i++){que->get();std::this_thread::sleep_for(std::chrono::microseconds(100));}
}
int main()
{Queue que;std::thread p(producer, &que);std::thread c(conducer, &que);p.join();c.join();return 0;
}

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

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

相关文章

c++位运算总结

在C中&#xff0c;位运算是对二进制位进行操作的运算&#xff0c;主要有以下几种&#xff1a; 1. 按位与&#xff08; & &#xff09;&#xff1a;两个操作数对应位都为1时&#xff0c;结果位才为1&#xff0c;否则为0。例如 3 & 5 &#xff0c; 3 二进制是 0000 0011…

1.1 计算机网络的概念

首先来看什么是计算机网络&#xff0c;关于计算机网络的定义并没有一个统一的标准&#xff0c;不同的教材有 不同的说法&#xff08;这是王道书对于计算机网络的定义&#xff09;&#xff0c;我们可以结合自己的生活经验去体会这个 定义。 可以用不同类型的设备去连接计算机网络…

用LLama factory时报类似Process 2504721 got signal: 1的解决方法

之前用nohup来远程跑LLama factory微调脚本&#xff0c;是没有问题的&#xff0c;但今天发现运行类似下面这个命令时&#xff0c; nohup llamafactory-cli train examples/train_qlora/qwen_lora.yaml 只要一关闭ssh session&#xff0c;就会终止训练&#xff0c;报类似&…

python常用内置时间函数+蓝桥杯时间真题

1.time 1.1 time.time() 时间戳指&#xff1a;1970年1月1日开始到现在所经过的秒数 import time print(time.time()) # 输出可得1970年1月1日开始到执行此代码所经过的秒数 1.2 time.localtime() 返回一个当前时间的时间对象&#xff0c;具体信息&#xff0c;并且可以单独…

一个用 C 语言打印出所有三位数水仙花数的程序

水仙花数&#xff08;Narcissistic number&#xff09;是指一个三位数&#xff0c;其各位数字的立方和等于该数本身。例如&#xff1a;153 是一个水仙花数&#xff0c;因为 (1^3 5^3 3^3 153)。 以下是一个用 C 语言打印出所有三位数水仙花数的程序&#xff1a; 代码实现 …

利用 VSCode 配置提升 vibe coding 开发效率

利用 VSCode 配置提升 vibe coding 开发效率 Vibe Coding&#xff08;氛围编程&#xff09;是一种基于AI的编程方法&#xff0c;其核心在于通过自然语言描述软件需求&#xff0c;再由大规模语言模型&#xff08;LLM&#xff09;自动生成代码&#xff0c;从而实现对传统手写编程…

练习题:110

目录 Python题目 题目 题目分析 需求理解 关键知识点 实现思路分析 代码实现 代码解释 函数定义&#xff1a; 计算值的总和&#xff1a; 测试函数&#xff1a; 运行思路 结束语 Python题目 题目 定义一个函数&#xff0c;接受一个字典作为参数&#xff0c;返回字…

处理 Linux 信号:进程控制与异常管理的核心

个人主页&#xff1a;chian-ocean 文章专栏-Linux 前言&#xff1a; 在 Linux 操作系统中&#xff0c;信号是用于进程间通信的一种机制&#xff0c;能够向进程发送通知&#xff0c;指示某些事件的发生。信号通常由操作系统内核、硬件中断或其他进程发送。接收和处理信号是 Li…

通信协议之串口

文章目录 简介电平标准串口参数及时序USART与UART过程引脚配置 简介 点对点&#xff0c;只能两设备通信只需单向的数据传输时&#xff0c;可以只接一根通信线当电平标准不一致时&#xff0c;需要加电平转换芯片&#xff08;一般从控制器出来的是信号是TTL电平&#xff09;地位…

Unity编辑器功能及拓展(1) —特殊的Editor文件夹

Unity中的Editor文件夹是一个具有特殊用途的目录&#xff0c;主要用于存放与编辑器扩展功能相关的脚本和资源。 一.纠缠不清的UnityEditor 我们Unity中进行游戏构建时&#xff0c;我们经常遇到关于UnityEditor相关命名空间丢失的报错&#xff0c;这时候&#xff0c;只得将报错…

工具类-csv文件导入数据库思路

首先&#xff0c;让我们来看下数据库建表语句&#xff1a; CREATE TABLE behavior_reports (id BIGINT PRIMARY KEY AUTO_INCREMENT COMMENT 报告ID,report_type VARCHAR(50) NOT NULL COMMENT 报告类型(daily, weekly, monthly),start_date DATE NOT NULL COMMENT 开始日期,e…

软件工程之软件开发模型(瀑布、迭代、敏捷、DevOps)

1. 瀑布模型&#xff08;Waterfall Model&#xff09; 定义与流程 瀑布模型是线性顺序的开发流程&#xff0c;包含需求分析、设计、编码、测试、维护等阶段&#xff0c;每个阶段完成后才能进入下一阶段&#xff0c;类似“瀑布流水”逐级推进。 核心特点 严格阶段划分&#…

FreeRTOS与RT-Thread内存分配对比分析

一、动态内存分配策略 ​FreeRTOS ​分配算法多样性&#xff1a;提供5种动态内存管理算法&#xff08;heap_1至heap_5&#xff09;&#xff0c;覆盖从简单到复杂的场景。例如&#xff1a; heap_1&#xff1a;仅支持分配不支持释放&#xff0c;适用于固定任务栈分配。heap_4&…

202519 | Mybatis-Plus

快速入门 MyBatis-Plus&#xff08;简称 MP&#xff09;是 MyBatis 的增强工具&#xff0c;它在 MyBatis 的基础上只做增强不做改变&#xff0c;简化了开发&#xff0c;提高了效率。以下是 MyBatis-Plus 的快速入门指南&#xff0c;帮助您快速上手使用。 1. 环境准备 JDK&…

Linux C语言调用第三方库,第三方库如何编译安装

在 Linux 环境下使用 C 语言调用第三方库时&#xff0c;通常需要先对第三方库进行编译和安装。以下为你详细介绍一般的编译安装步骤&#xff0c;并给出不同类型第三方库&#xff08;如使用 Makefile、CMake 构建系统&#xff09;的具体示例。 一般步骤 1. 获取第三方库源码 …

linux基本命令(1)--linux下的打包命令 -- tar 和gzip

tar 解压 &#xff0c;打包 语法&#xff1a;tar [主选项辅选项] 文件或者目录 使用该命令时&#xff0c;主选项是必须要有的&#xff0c;它告诉tar要做什么事情&#xff0c;辅选项是辅助使用的&#xff0c;可以选用。 主选项&#xff1a; c 创建新的档案文件。如果用户想备…

Python 序列构成的数组(对序列使用+和_)

对序列使用和* Python 程序员会默认序列是支持 和 * 操作的。通常 号两侧的序列由 相同类型的数据所构成&#xff0c;在拼接的过程中&#xff0c;两个被操作的序列都不会被 修改&#xff0c;Python 会新建一个包含同样类型数据的序列来作为拼接的结果。 如果想要把一个序列…

[ C语言 ] | 从0到1?

目录 认识计算机语言 C语言 工欲善其事必先利其器 第一个C语言代码 这一些列 [ C语言 ] &#xff0c;就来分享一下 C语言 相关的知识点~ 认识计算机语言 我们说到计算机语言&#xff0c;语言&#xff0c;就是用来沟通的工具&#xff0c;计算机语言呢&#xff1f;就是我们…

【通道注意力机制】【SENet】Squeeze-and-Excitation Networks

0.论文摘要 卷积神经网络建立在卷积操作的基础上&#xff0c;通过融合局部感受野内的空间和通道信息来提取有意义的特征。为了增强网络的表示能力&#xff0c;最近的一些方法展示了增强空间编码的好处。在本研究中&#xff0c;我们专注于通道关系&#xff0c;并提出了一种新颖…

kubernetes Calico(CNI) NetworkPolicy 流量管理 设置networkpolicy 策略 下集

1、kubernetes 网络策略&#xff08;网络隔离策略&#xff09; Network Policy 是 Kubernetes 中用于控制 Pod 之间网络通信的一种机制。它通过定义规则&#xff0c;限制哪些 Pod 或外部实体可以与目标 Pod 通信&#xff08;基于标签、命名空间、端口等&#xff09;。Network …