C++ 线程库

news2025/1/11 3:03:54

文章目录

  • thread 创建
  • mutex
    • mutex
    • recursive_mutex
    • timed_mutex
    • lock_guard
  • 原子操作
    • atomic
  • 条件变量
    • condition_variable
  • 其他线程安全问题
    • shared_ptr
    • 单例模式

C++ 线程库是 C++11 标准中引入的一个特性,它使得 C++ 在语言级别上支持多线程编程,不需要依赖第三方库或操作系统 API。C++ 线程库主要包括以下几个部分:

  • std::thread:创建和管理子线程的类
  • std::mutex:互斥锁,用于保护共享数据的访问
  • std::condition_variable:条件变量,用于同步线程之间的状态
  • std::futurestd::promise:异步任务和结果的封装
  • std::async:启动异步任务的函数

thread 创建

thread

std::thread 类的构造函数:

img

std::thread 是一个类,它表示一个可执行的线程。你可以用它来创建和管理子线程,让它们并发地执行不同的任务。std::thread 有以下几个特点:

  • 它不可拷贝,只能移动。如上(3)(4)
  • 它可以被 join 或 detach,join 表示等待线程结束,detach 表示让线程自行运行
  • 它有一个唯一的标识符 std::thread::id,可以用来区分不同的线程

img

在命名空间 this_thread下有一个 get_id 函数,可以帮助我们获取线程 id

yield:可以使当前线程让出时间片

sleep_until:sleep 到特定时间点

sleep_for:sleep 一段时间


一个双线程循环打印的例子:

#include <iostream>
#include <thread>

using namespace std;

void print(int n) {
	for (int i = 0; i < n; ++i) {
		cout << this_thread::get_id() << ":" << i << endl; // 打印 [线程id]:i
		this_thread::sleep_for(chrono::seconds(1)); // sleep 1 秒
	}
}

int main() {
	thread t1(print, 100);
	thread t2(print, 100);

	t1.join();
	t2.join();

	return 0;
}

mutex

mutex

例一:给上面的代码加锁

#include <iostream>
#include <thread>
#include <mutex>

using namespace std;

mutex mtx; // 创建一把锁

void print(int n) {
	mtx.lock();	// 加锁
	for (int i = 0; i < n; ++i) {
		cout << this_thread::get_id() << ":" << i << endl; // 打印 [线程id]:i
		this_thread::sleep_for(chrono::milliseconds(100)); // sleep 100 ms
	}
	mtx.unlock(); // 解锁
}

int main() {
	thread t1(print, 100);
	thread t2(print, 100);

	t1.join();
	t2.join();

	return 0;
}

例二:

如果锁是创建在局部,则要通过函数参数传入

由于锁不支持拷贝,所以必须通过引用传入,又由于可变模板参数会默认识别成传值,所以必须先使用 ref 来强制转换成引用。

#include <iostream>
#include <thread>
#include <mutex>

using namespace std;

void print(int n, mutex& mtx) {
	mtx.lock();	// 加锁
	for (int i = 0; i < n; ++i) {
		cout << this_thread::get_id() << ":" << i << endl; // 打印 [线程id]:i
		this_thread::sleep_for(chrono::milliseconds(100)); // sleep 100 ms
	}
	mtx.unlock(); // 解锁
}

int main() {
	mutex mtx; // 创建一把锁
	thread t1(print, 100, ref(mtx)); // 必须使用 ref 来传引用
	thread t2(print, 100, ref(mtx));

	t1.join();
	t2.join();

	return 0;
}

例三:

配合 vectorlambda 使用

#include <iostream>
#include <thread>
#include <mutex>
#include <vector>

using namespace std;

int main() {
	mutex mtx; // 创建一把锁
	int x = 0, n = 10, m;
	cin >> m;
	vector<thread> v(m);
	for (int i = 0; i < m; ++i) {
		v[i] = thread([&]() {
			mtx.lock();	// 加锁
			for (int i = 0; i < n; ++i) {
				cout << this_thread::get_id() << ":" << i << endl; // 打印 [线程id]:i
				this_thread::sleep_for(chrono::milliseconds(100)); // sleep 100 ms
			}
			mtx.unlock(); // 解锁
		});
	}

	for (auto& t : v) {
		t.join();
	}

	return 0;
}

recursive_mutex

如果在递归函数中使用普通的互斥锁会造成死锁,因为当一个线程执行到 unlock 之前就可能会递归调用自己,然后从头开始执行,当再次遇到 lock 时,由于之前没有执行 unlock,就会死锁。

recursive_mutex 可以防止递归造成的死锁,它在每次 lock 的时候会判断是不是当前线程,如果是,就不用阻塞,可以继续向下执行了。

timed_mutex

定时互斥锁,该类锁可以设置锁住的时间。

由它的两个成员实现 try_lock_for 和 try_lock_until.

到时间后我们不需要手动解锁,而是自动解锁。

lock_guard

智能锁,通过 RAII 技术实现。

它的实现类似于如下方法:

template<class Lock>
class LockGuard {
public:
	LockGuard(Lock& lk) : _lock(lk) {
		_lock.lock();
	}

	~LockGuard() {
		_lock.unlock();
	}
private:
	Lock& _lock;
};

例:

int main() {
	mutex mtx; // 创建一把锁
	atomic<int> x = 0;
	//int x = 0;
	int n = 100, m;
	cin >> m;
	vector<thread> v(m);
	for (int i = 0; i < m; ++i) {
		v[i] = thread([&]() {
			for (int i = 0; i < n; ++i) {
				lock_guard<mutex> lk(mtx); // 智能锁
				cout << this_thread::get_id() << ':' << i << endl;
				this_thread::sleep_for(chrono::microseconds(100));
			}
		});
	}

	for (auto& t : v) {
		t.join();
	}

	cout << x << endl;

	return 0;
}

注:unique_guardlock_guard 多一个支持手动加锁的解锁的功能。

原子操作

atomic

我们知道,++ 操作不是线程安全的,要避免多线程同时修改造成的数据不一致的问题,可以通过加锁来解决。

但是互斥锁会产生上下文切换的开销,容易产生死锁。更好的解决方法是使用原子操作。


CAS (Compare & Set,或 Compare & Swap)是一种原子操作,也就是说它是一个不会被其他线程打断的操作。CAS 有三个操作数:内存值 V,旧的预期值 A,要修改的新值 B。CAS 的过程是这样的:先比较内存值 V 和旧的预期值 A 是否相等,如果相等,就用新值 B 替换内存值 V;如果不相等,就放弃操作或者重试。CAS 可以用于实现无锁算法,避免多线程同时修改同一数据时产生的数据不一致问题。

C++11 中的 atomic 类就是 CAS 的一种实现。

例:

#include <iostream>
#include <thread>
#include <vector>

using namespace std;

int main() {
    //int x = 0;
	atomic<int> x = 0;
	int n = 100000, m;
	cin >> m;
	vector<thread> v(m);
	for (int i = 0; i < m; ++i) {
		v[i] = thread([&]() {
			for (int i = 0; i < n; ++i) {
				++x;				// 原子操作
			}
		});
	}

	for (auto& t : v) {
		t.join();
	}

	cout << x << endl;

	return 0;
}

条件变量

condition_variable

条件变量是一种同步原语,用于让线程在某个条件发生时才继续执行。 条件变量与互斥锁配合使用,让线程在等待条件时释放锁,从而避免竞争状态。条件变量提供了一种原子操作,即解锁并睡眠的操作,以及唤醒并加锁的操作。条件变量有两个动作:等待(wait)和通知(notify)。等待动作会让线程挂起,并释放已经持有的锁。通知动作会唤醒一个或多个等待的线程,并让它们重新获取锁。

在 C++11 中,你可以使用 std::condition_variable 类来创建和操作条件变量。你需要配合一个互斥锁(std::mutex)和一个谓词(std::function<bool()>)来使用条件变量。你可以调用条件变量的成员函数,如wait()notify_one()notify_all()等来实现线程间的同步。


img

predicate(2) 是增加了谓词的版本,pred 是一个 std::function<bool()>,它返回 false 则表示阻塞,返回 true 时解除阻塞。

:创建两个线程,交替打印 0~100,如线程 t2 先打印 0,则下面必须是另一个线程 t1 打印 1,然后 t2 打印 2 …

这是一个运用条件变量解决的经典场景,我们可以让一个线程打印完一个数之后立马通知另一个线程,从而保证两个线程交替进行。

#include <iostream>
#include <thread>
#include <mutex>
#include <condition_variable>

using namespace std;

int main() {
	int i = 0;
	int n = 100;
	mutex mtx;
	condition_variable cv;
	bool ready = true; // 开始标志,初始为 true,可以使线程 t2 先打印

	thread t1([&]() {
		while (i < n) {
			unique_lock<mutex> lock(mtx); // 条件变量需要的互斥锁
			cv.wait(lock, [&ready]() {return !ready; }); // 如果 ready 为 true 则阻塞
			cout << this_thread::get_id() << ":" << i << endl;
			++i;

			ready = true;	// 更改条件
			cv.notify_one();// 唤醒另一个线程
		}
	});

	thread t2([&]() {
		while (i < n) {
			unique_lock<mutex> lock(mtx);
			cv.wait(lock, [&ready]() {return ready; }); // 如果 ready 为 false 则阻塞
			cout << this_thread::get_id() << ":" << i << endl;
			++i;

			ready = false;	// 更改条件
			cv.notify_one();// 唤醒另一个线程
		}
	});

	t1.join();
	t2.join();

	return 0;
}

例2:生产者消费者模型

#include <iostream>
#include <thread>
#include <mutex>
#include <condition_variable>
#include <queue>

std::mutex mtx; // 互斥锁
std::condition_variable cv; // 条件变量
std::queue<int> q; // 共享队列
bool finished = false; // 结束标志

void producer(int n) {
    for (int i = 0; i < n; ++i) {
        std::unique_lock<std::mutex> lock(mtx); // 加锁
        std::this_thread::sleep_for(std::chrono::milliseconds(100));
        q.push(i); // 生产数据
        std::cout << "produced " << i << "\n";
        lock.unlock(); // 解锁
        cv.notify_one(); // 通知消费者
    }
    {
        std::unique_lock<std::mutex> lock(mtx); // 加锁
        finished = true; // 设置结束标志
    }
    cv.notify_all(); // 通知所有消费者
}

void consumer() {
    while (true) {
        std::unique_lock<std::mutex> lock(mtx); // 加锁
        cv.wait(lock, [] {return finished || !q.empty(); }); // 等待条件成立:结束或队列非空
        if (finished && q.empty()) {
            break; // 结束且队列空,退出循环
        }
        std::this_thread::sleep_for(std::chrono::milliseconds(100));
        int x = q.front(); // 取出数据
        q.pop();
        std::cout << std::this_thread::get_id() << ": consumed " << x << "\n";
    }
}

int main() {
    std::thread t1(producer, 10); // 创建生产者线程,生产10个数据
    std::thread t2(consumer); // 创建消费者线程1
    std::thread t3(consumer); // 创建消费者线程2

    t1.join();
    t2.join();
    t3.join();

    return 0;
}

运行结果

img

其他线程安全问题

shared_ptr

Q:shared_ptr 是线程安全的吗?

A:shared_ptr 的引用计数保证是线程安全的,但访问资源不是,如果你想在多个线程之间共享同一个 shared_ptr 指向的对象,你需要使用互斥锁或原子操作来保护它。


单例模式

饿汉模式:

  • 特点:
    1. 不允许随便创建对象,构造函数私有
    2. 防拷贝
    3. main 函数之前就创建初始化对象
  • 缺点:
    1. 对象初始化麻烦,影响程序启动速度
    2. 多个单例类,如果有依赖顺序关系,无法控制

懒汉模式:

  • 特点:
    1. 不允许随便创建对象,构造函数私有
    2. 防拷贝
    3. 第一次使用对象时,创建对象
  • 缺点:
    1. 多个线程使用,第一次调用存在竞争的线程安全问题。

所以饿汉模式不用考虑线程安全问题,而懒汉模式有线程安全问题

下面是一个懒汉模式的代码示例:

class Singleton {
public:
	static Singleton* GetInstance() {
		if (_spInst == nullptr) {
			_spInst = new Singleton;
		}
		return _spInst;
	}
private:
	Singleton() {}
	Singleton(const Singleton&) = delete;

	static Singleton* _spInst;
};

Singleton* Singleton::_spInst = nullptr;

如果有多个线程同时调用 GetInstance,可能会创建多个对象实例,违反了单例模式的原则。

这个问题可以通过加锁解决

下面的代码是一个双重检查加锁的懒汉模式的实现,双重检查加锁是一种用于减少获取锁的开销的软件设计模式。程序先检查锁定条件,只有当检查表明需要锁定时才获取锁,然后检查是否已经实例化对象,防止创建多个对象。

class Singleton {
public:
	static Singleton* GetInstance() {
		if (_spInst == nullptr) { // 第一重检查
			std::unique_lock<std::mutex> lock(_mtx);
			if (_spInst == nullptr) { // 第二重检查
				_spInst = new Singleton;
			}
		}
		return _spInst;
	}
private:
	Singleton() {}
	Singleton(const Singleton&) = delete;

	static Singleton* _spInst;
	static std::mutex _mtx;
};

Singleton* Singleton::_spInst = nullptr;
std::mutex Singleton::_mtx;

你会发现这两重检查缺一不可,如果缺少了第一重检查,那么每次调用 GetInstance 都会加锁解锁,增加了获取锁的开销。如果缺少了第二重检查,那么两个线程会竞争一把锁,当其中一个线程加锁之后,另一个线程会阻塞,当一个线程完成了对象的实例化,并释放锁,另一个线程就会获取锁,此时如果没有第二重检查,那么它就会再创建一个对象,违反了单例模式的原则。


第二种线程安全的懒汉模式的实现方式:

它利用了静态局部对象只会在第一次调用时初始化特性。

class Singleton {
public:
	static Singleton* GetInstance() {
		static Singleton _s;
		return &_s;
	}
private:
	Singleton() {}
	Singleton(const Singleton&) = delete;
	Singleton& operator=(Singleton const&) = delete;
};

但是这种实现方式有一个缺点,它在 C++11 之前不能保证是线程安全的,因为 C++11 之前局部静态对象的构造函数并不能保证是线程安全的。

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

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

相关文章

unity开发知识点小结01

unity对象生命周期函数 Awake():最早调用&#xff0c;所以可以实现单例模式 OnEnable&#xff08;&#xff09;&#xff1a;组件激活后调用&#xff0c;在Awake后调用一次 Stat&#xff08;&#xff09;&#xff1a;在Update&#xff08;&#xff09;之前&#xff0c;OnEnable…

【C++知识点】位运算

✍个人博客&#xff1a;https://blog.csdn.net/Newin2020?spm1011.2415.3001.5343 &#x1f4da;专栏地址&#xff1a;C/C知识点 &#x1f4e3;专栏定位&#xff1a;整理一下 C 相关的知识点&#xff0c;供大家学习参考~ ❤️如果有收获的话&#xff0c;欢迎点赞&#x1f44d;…

海思嵌入式开发-005-OpenHarmony源码编译问题

海思嵌入式开发-005-OpenHarmony源码编译问题一、问题描述二、解决方案2.1解决原理2.2获取OpenHarmony 3.1.1 Release源码2.3最后解决问题&#xff0c;编译成功。一、问题描述 按照链接拉取master源码&#xff0c;出现如下问题&#xff0c;打开build.log文件 提示相应位置的文…

Servlet详细教程

文章目录Servletservlet 简介Servlet 入门案例页面编写页面提交 get 请求Servlet 和 Tomcat 关系servlet-apiget 和 post 请求Servlet 生命周期案例HttpServletRequest 接口简介文件上传FileServlet 类Servlet servlet 简介 servlet 全称为 server applet 是服务器的小程序&am…

龙腾iSharedisk无盘系统 v1.8 Build 20230207 Crack

龙腾 iShareDisk无盘系统是一款高品质的 无盘启动和VHD离线启动系统。其功能满足目前校园、网咖、企业、酒店、证券、服务业、KTV、包厢VOD的需求&#xff0c;其可以 自行选择部署有盘或者无盘&#xff0c;实现Windows全系列产品无盘/VHD 启动的一体化解决方案&#xff01; …

【Storm】【七】Storm三种打包方式对比分析

Storm三种打包方式对比分析 一、简介二、mvn package三、maven-assembly-plugin插件四、maven-shade-plugin插件五、结论六、打包注意事项一、简介 在将 Storm Topology 提交到服务器集群运行时&#xff0c;需要先将项目进行打包。本文主要对比分析各种打包方式&#xff0c;并…

MyBatis - 14 - 分页插件的配置及使用

文章目录1、分页插件配置&#xff08;1&#xff09;在pom.xml中添加依赖&#xff08;2&#xff09;在MyBatis的核心配置文件中配置插件2、分页插件的使用回顾Mysql分页功能MyBatis分页插件的使用测试显示第1页&#xff0c;每页显示4条数据&#xff0c;打印page对象测试获取分页…

A. Linova and Kingdom(dfs + 贪心)

A. Linova and Kingdom&#xff08;dfs 贪心&#xff09;一、问题二、思路三、代码一、问题 二、思路 这道题的大意就是&#xff0c;给我们一棵树&#xff0c;我们需要在树上选择kkk个点&#xff0c;然后让kkk个信使从我们选取的kkk个点向第一个点出发。 我们把我们选取的k个…

Verdaccio 搭建私有 npm 仓库

背景 公司内部封装业务相关的组件库&#xff0c;工具库&#xff0c;希望统一管理和维护&#xff0c;在多个项目中都能使用&#xff0c;同时希望不公开&#xff0c;只在局域网中使用。所以&#xff0c;需要搭建私有 npm 仓库 Verdaccio verdaccio 是一个能够创建私有 registr…

vue:vue2与vue3的区别

一、背景 vue2是指的2.X vue3是指的3.0以及更新的版本&#xff08;3.2版本在script标签里可以写setup&#xff0c;极大的简化了开发&#xff09; 本文对比两者区别。 二、官网 生命周期选项 | Vue.js API 参考 | Vue.js Vue.js - 渐进式 JavaScript 框架 | Vue.js Vue.…

Redis学习【11】之分布式系统

文章目录一 数据分区算法1.1 顺序分区1.1.1 轮询分区算法1.1.2 时间片轮转分区算法1.1.3 数据块分区算法1.1.4 业务主题分区算法1.2 哈希分区1.2.1 节点取模分区算法1.2.2 一致性哈希分区算法1.2.3 虚拟槽分区算法二 分布式系统环境搭建与运行2.1 系统搭建2.1.1 系统架构2.1.2 …

物理层的概述(可以说是对王道计算机网络的笔记)

目录前言物理层概述基本概念数据通信基础知识数据通信相关术语三种通信方式两种传输方式码元&#xff0c;速率、波特、带宽**练习题**奈氏准则和香农定理奈氏准则&#xff08;奈奎斯特定理&#xff09;香浓定理结尾前言 本章内容讲述了物理层的概念,也是我上个星期上课的内容&…

现代检测技术-期末复习

文章目录差动结构的优点偏差/零位/微差法的应用偏差法测量零位法测量微差法测量格罗布斯准则&#xff08;作业题&#xff09;最小二乘法自相关/互相关算法的应用&#xff08;教材和课件案例&#xff09;自相关性分析互相关分析&#xff1a;电子计数器测频法&#xff08;作业题&…

第53章 短信验证服务和登录的前端定义实现

1 向src\router\index.js添加定义 { path: /LoginSms, name: 手机号登录, component: () > import(../views/LoginSmsView.vue) }, { path: /Users/Register, name: 用户注册, component: () > import(../views/Users/RegisterView.vue), }, 2 向src\common\http.api.js添…

Javascript借用原型对象继承父类型方法

借用原型对象继承父类型方法 目的: 儿子继承父类属性和方法&#xff0c;父类之后新增的方法不会被儿子继承。 前言&#xff1a; 先理解一个问题&#xff1a; Son.prototype Father.prototype; 这一操作相当于把Son的原型对象指向Father。 意味着Son的prototype的地址与Fa…

Vue基础学习 v-指令(2) 本地应用(记事本)

v-bind 设置元素的属性&#xff08;比如&#xff1a;src&#xff0c;title&#xff0c;class&#xff09; v-bind:属性名值 <div id"app"><img v-bind:src"imgSrc" alt"" v-bind:title"imgTitle"></div><scrip…

数学建模(一):LP 问题

文章目录数学建模&#xff08;一&#xff09;&#xff1a;LP 问题一、 MATLAB求解二、 Python 求解数学建模&#xff08;一&#xff09;&#xff1a;LP 问题 在人们的生产实践中&#xff0c;经常会遇到如何利用现有资源来安排生产&#xff0c;以取得最大经济效益的问题。此类问…

关于分布式事务的理解

关于分布式事务的理解 分布式事务之前先简单介绍下介于本地事务和分布式事务之间的两个事务&#xff1a;全局事务&#xff08;Global Transactions&#xff09;和共享事务&#xff08;Share Transactions&#xff09;的原理与实现。 先给全局事务做个限定&#xff1a;一种适用…

JVM运行时数据区划分

Java内存空间 内存是非常重要的系统资源&#xff0c;是硬盘和cpu的中间仓库及桥梁&#xff0c;承载着操作系统和应用程序的实时运行。JVM内存布局规定了JAVA在运行过程中内存申请、分配、管理的策略&#xff0c;保证了JVM的高效稳定运行。不同的jvm对于内存的划分方式和管理机…

使用secure crt连接ensp中虚拟设备

0 前言 ensp中虚拟设备如路由器、防火墙等本质上是 virtualbox中运行的虚机&#xff0c;因此可通过 telnet 连接 127.0.0.1 及对应端口方式连接到ensp中设备&#xff1b; 1 连接方法 1.1 查看设备所监听端口 设备图标上&#xff0c;右键 设置 点击 配置&#xff0c;可查看到…