【C++特性及类库】多线程

前言

测试环境:VS2019 如果做特殊说明,是C++14。

注意:和C#不一样,stl会在构造函数中启动线程,无需手动启动,join是等待函数结束。

cpp 复制代码
long long g_x = 0;
void fun() {
	for (int i = 0; i < 1000 * 1000*1000*2;i++) {
		g_x++;
	}
}
int main()
{
	std::thread th1(fun);
	std::thread th2(fun);
	th1.join();
	th2.join();
}

以上代码执行完毕后,应该是 4 × 10 9 , 实际上是 2.3 × 10 9 4\times 10^9,实际上是2.3 \times 10^9 4×109,实际上是2.3×109。g_x++,可以看成t=g_x,t++,g_x=t。出错的情况,如下:

线程一执行第一行,t=0。

线程二执行第一行,t=0。

线程一二执行t++。

线程一执行g_x=t。g_x等于1。

线程二执行g_x=t。g_x等于1。

Window下,有四种同步异步的方法,临界区(当前进程)、互斥量、事件对象(可发通知消息)、信号量(可以有多个资源)。

同步异步方法

std::mutex

stl的互斥量,在windows下实际是临界区。加锁后,速度变慢,故循环次数变成十分之一。

cpp 复制代码
#include <thread>
#include <iostream>
#include <mutex>

std::mutex m;
long long g_x = 0;
void fun() {
	for (int i = 0; i < 1000 * 1000*100*2;i++) {
		m.lock();
		g_x++;
		m.unlock();
	}
}
int main()
{
	std::thread th1(fun);
	std::thread th2(fun);
	th1.join();
	th2.join();
}

条件变量

cpp 复制代码
#include <condition_variable>
#include <mutex>
#include <thread>
#include <iostream>

std::mutex mtx;
std::condition_variable cv;
bool ready = false;

void thread1() {
	std::this_thread::sleep_for(std::chrono::milliseconds(1));
	std::lock_guard<std::mutex> lk(mtx);
	ready = true;
	cv.notify_one();
}

void thread2() {
	std::unique_lock<std::mutex> lk(mtx);
	cv.wait(lk, [] { return ready; }); // 阻塞直到 ready == true
	std::cout << "worker: got ready\n";
}

int main() {
	std::thread t1(thread1), t2(thread2);
	t1.join();
	t2.join();
}

worker: got ready

改成以下代码

cpp 复制代码
int main() {
	std::thread t1(thread1), t2(thread2) , t3(thread2);
	t1.join();
	t2.join();
	t3.join();
}

输出不变,t3永远不会结束。

cpp 复制代码
cv.notify_one();

改成

cpp 复制代码
cv.notify_all();

std::shared_mutex

std::shared_mutex 在 Windows 上的开销比 std::mutex 大。shared_mutex 允许多个读者并行进入临界区,减少了线程的无效阻塞,从而提高了读密集型任务的吞吐量。

测试了若干小程序,实际用时相差不大。

cpp 复制代码
#include <iostream>
#include <thread>
#include <shared_mutex>
#include <vector>
#include "ConsoleApplication1.h"

volatile long long t = 0;
std::string str;

std::shared_mutex rw_mtx;
void worker1() {
	for (int i = 0;i < 100;i++) {
		std::unique_lock<std::shared_mutex>(rw_mtx);
		str = str + 'a';
	}
}
void worker2() {
	for (int i = 0;i < 300;i++) {
		std::shared_lock<std::shared_mutex>(rw_mtx);
		std::this_thread::sleep_for(std::chrono::milliseconds(1));
		for (const auto& ch : str)
		{
			t += ch;
		}
	}
}

void Test()
{
	std::thread th1(worker1);
	std::vector<std::thread> ths;
	for (int i = 0;i < 500;i++) {
		ths.emplace_back(std::thread(worker2));
	}
	th1.join();
	for (auto& th : ths) {
		th.join();
	}
}

int main() {
	auto start = std::chrono::high_resolution_clock::now();

	Test();
	Test();
	Test();
	Test();
	Test();
	auto end = std::chrono::high_resolution_clock::now();
	auto elapsed = std::chrono::duration_cast<std::chrono::milliseconds>(end - start).count();
	std::cout << "总耗时: " << elapsed << " ms" << std::endl;
	std::cout << "t: " <<t << std::endl;
	int tmp;
	std::cin >> tmp;
	return 0;
}

总耗时: 23855 ms

t: 21825000000

cpp 复制代码
#include <iostream>
#include <thread>
#include <shared_mutex>
#include <vector>
#include "ConsoleApplication1.h"

volatile long long t = 0;
std::string str;


std::shared_mutex rw_mtx;
void worker1() {
	for (int i = 0;i < 100;i++) {
		std::unique_lock<std::shared_mutex>(rw_mtx);
		str = str + 'a';
	}
}
void worker2() {
	for (int i = 0;i < 300;i++) {
		std::shared_lock<std::shared_mutex>(rw_mtx);
		std::this_thread::sleep_for(std::chrono::milliseconds(1));
		for (const auto& ch : str)
		{
			t += ch;
		}
	}
}

void Test()
{
	std::thread th1(worker1);
	std::vector<std::thread> ths;
	for (int i = 0;i < 500;i++) {
		ths.emplace_back(std::thread(worker2));
	}
	th1.join();
	for (auto& th : ths) {
		th.join();
	}
}

int main() {
	auto start = std::chrono::high_resolution_clock::now();

	Test();
	Test();
	Test();
	Test();
	Test();
	auto end = std::chrono::high_resolution_clock::now();
	auto elapsed = std::chrono::duration_cast<std::chrono::milliseconds>(end - start).count();
	std::cout << "总耗时: " << elapsed << " ms" << std::endl;
	std::cout << "t: " <<t << std::endl;
	int tmp;
	std::cin >> tmp;
	return 0;
}

总耗时: 24311 ms

t: 21825000000

锁

std::lock_guard 和 std::unique_lock 都是 C++ 标准库 提供的RAII 风格互斥锁管理工具,RAII 全称是 Resource Acquisition Is Initialization,通俗地说在构造函数中加锁,析构函数中解锁。

std::lock_guard

只有构造函数和析构,拷贝构造函数和等于被删除。

cpp 复制代码
#include <thread>
#include <iostream>
#include <mutex>

std::mutex m;
long long g_x = 0;
void fun() {
	for (int i = 0; i < 1000 * 1000*100*2;i++) {
		std::lock_guard<std::mutex> lock(m);
		g_x++;
	}
}
int main()
{
	std::thread th1(fun);
	std::thread th2(fun);
	th1.join();
	th2.join();
}

加锁后,结果正确。

try_lock的应用场景,有些次要任务可以忽略。比如:某些设备需要换料,故每60分钟一定有30秒空闲,利用空闲可以做一下维护工作。比如:某些次要日志。

std::unique_lock

换成唯一锁也正确。

cpp 复制代码
		std::unique_lock<std::mutex> lock(m);
cpp 复制代码
    _NODISCARD_CTOR unique_lock(_Mutex& _Mtx, adopt_lock_t) 
    unique_lock(_Mutex& _Mtx, defer_lock_t) noexcept
    _NODISCARD_CTOR unique_lock(_Mutex& _Mtx, try_to_lock_t)

以上三个版本的构造函数,分别是:加锁,不加锁,尝试加锁。

接下来三个版本,是时间有关。

移动构造函数和移动赋值,容许转移控制权。

赋值构造函数和赋值函数,擅长。

手动加锁、解锁。

交换函数。

信号量:counting_semaphore

C++20才支持。

cpp 复制代码
#include <iostream>
#include <thread>
#include <semaphore>
#include <vector>

std::counting_semaphore<2> sema(2);  // 创建一个初始值为2的信号量


void worker1() {
	sema.acquire();  // 等待信号量
	std::cout << "线程开始!" << std::endl;
	std::this_thread::sleep_for(std::chrono::milliseconds(1000));
	std::cout << "线程结束"<< std::endl;
	sema.release();
}
void worker2() {
	sema.acquire();  // 等待信号量
	std::cout << "线程开始!" << std::endl;
	std::this_thread::sleep_for(std::chrono::milliseconds(2000));
	std::cout << "线程结束" << std::endl;
	sema.release();
}

int main() {

	std::vector<std::thread> ths;
	for (int i = 0;i < 10;i++) {
		ths.emplace_back(std::thread(worker1));
		ths.emplace_back(std::thread(worker2));
	}
	for (auto& th : ths) {
		th.join();		
	}
	return 0;
}

线程开始!线程开始!

线程结束

线程开始!

线程结束

线程开始!

线程结束

线程开始!

线程结束

线程开始!

线程结束

线程开始!

线程结束

线程开始!

线程结束

线程开始!

线程结束

线程开始!

线程结束

线程开始!

线程结束

线程开始!

线程结束

线程开始!

线程结束

线程开始!

线程结束

线程开始!

线程结束

线程开始!

线程结束

线程开始!

线程结束

线程开始!

线程结束

线程开始!

线程结束

线程开始!

线程结束

线程结束

原子操作 std::atomic

x++是三个原则操作,t=x, t++,x=t。如果能统一成一个原子操作,则无需同步。

cpp 复制代码
long long g_x = 0;
std::atomic<long long> g_y;
void fun() {
	for (int i = 0; i < 300 * 1000 * 1000 ;i++) {
		g_x++;
		g_y++;
	}
}
int main()
{
	std::thread th1(fun);
	std::thread th2(fun);
	th1.join();
	th2.join();
}

g_y正确,g_x错误。

cpp 复制代码
	long long tmp = 0;
	g_y.compare_exchange_strong(tmp, 1);
	//g_y=1,tmp=0;
	g_y.compare_exchange_strong(tmp, 1);
	//g_y=1,tmp=1

无锁链表

cpp 复制代码
#include <iostream>
#include <thread>
#include <shared_mutex>
#include <vector>
#include <set>
#include <assert.h>

struct Node {
	Node* next;
	int d;
};
std::atomic<Node*> head = nullptr;
void Push(int x) {
	Node* cur = new Node;
	cur->d = x;
	cur->next = head.load();
	while (!head.compare_exchange_strong(cur->next, cur));
}
void Pushs(int begin) {
	for (int i = 0;i < 1000;i++) {
		{Push(begin + i);}
	}
}
int main()
{
	const int cnt=1000;
	std::vector<std::thread> ths;
	for (int i = 0; i < cnt; i++) {
		ths.emplace_back(std::thread([&](int x) {Pushs(1000 * x);}
		, i));
	}
	for (auto& th : ths) {
		th.join();
	}
	std::set<int> s;
	for (auto tmp = head.load(); tmp != nullptr; tmp = tmp->next) {
		s.emplace(tmp->d);
	}
	assert(s.size()== cnt*1000);
	assert(0 == *s.begin());
	assert(1000 * cnt - 1 == *s.rbegin());
	getchar();
}

std::call_once / std::once_flag

cpp 复制代码
#include <mutex>
#include <iostream>

std::once_flag flag;

void init() {
    // 初始化工作
    std::cout << "初始化" << std::endl;
}

void thread_func() {
    std::call_once(flag, init);  // 多个线程同时调用,init 只执行一次
}

int main() {
    std::thread t1(thread_func);
    std::thread t2(thread_func);
    std::thread t3(thread_func);
    t1.join(); t2.join(); t3.join();
}

异常后会重新执行

cpp 复制代码
#include <mutex>
#include <iostream>

std::once_flag flag;

int cnt = 0;
void init() {
	if (cnt++ < 2) {
		throw("初始化失败");
	}
    // 初始化工作
    std::cout << "初始化" << std::endl;
}

void thread_func() {
	try
	{
		std::call_once(flag, init);  // 多个线程同时调用,init 只执行一次
	}
	catch(...){

	}
}
int main() {
    std::thread t1(thread_func);
    std::thread t2(thread_func);
    std::thread t3(thread_func);
    t1.join(); t2.join(); t3.join();
}

Meyers Singleton(函数局部静态变量)

C++11及更高版本,保证局部静态变量的现场安全。

cpp 复制代码
#include <mutex>
#include <iostream>

class CLog {
public:
	static CLog Log() {
		static CLog log;
		return log;
	}
	CLog() {
		std::cout << "构造函数";
	}
};
void thread_func() {
	CLog::Log();
}
int main() {
    std::thread t1(thread_func);
    std::thread t2(thread_func);
    std::thread t3(thread_func);
    t1.join(); t2.join(); t3.join();

std::async

cpp 复制代码
#include <mutex>
#include <iostream>
#include <future>

int main() {
	auto fun = [] {
		return 1;
	};
	auto fut = std::async(fun);
	auto x = fut.get();
	std::cout << x << std::endl;
}

会启动新线程,运行结果1。

自动处理结果

cpp 复制代码
#include <mutex>
#include <iostream>
#include <future>

int main() {
	for (int i = 0;i < 3;i++)
	{
		std::promise<int> prom;
		auto fut = prom.get_future();
		std::thread t([&prom,i] {
			std::this_thread::sleep_for(std::chrono::seconds(i+1));
			prom.set_value(18);
			});

		if (fut.wait_for(std::chrono::seconds(2)) == std::future_status::ready) {
			std::cout << fut.get() << std::endl;
		}
		else {
			std::cout << "超时" << std::endl;
		}
		t.join();
	}
}

18

18

超时

扩展阅读

计算几何为骨,排样优化为魂
作品一:亲士套料工具箱 作品二: 亲士CAD工具箱
经典文章推荐:二维排样
万物皆数学
查阅鄙人的博文,请点击博文下载学院导航
活到老,学到老。明朝中后期,大约50%的进士能当上堂官(副部及更高);能当上堂官的举人只有十余人。
子墨子言之:事无终始,无务多业。也就是我们常说的专业的人做专业的事。

测试环境

操作系统:win7 开发环境: VS2019 C++17

或者 操作系统:win10 开发环境: VS2022 C++17

如无特殊说明,本算法用**C++**实现。

相关推荐
多弗朗皮卡丘40 分钟前
C++ 哈希(下)
c++·算法·哈希算法
dtq04241 小时前
C++3 - 分支语句if和string变量
开发语言·c++·学习
jimy11 小时前
c++类成员访问只看静态类型
开发语言·c++
156082072191 小时前
银河麒麟 Qt 可执行文件打包说明
开发语言·驱动开发·qt
SEO_juper1 小时前
手写 llms.txt 验证器:让 AI 更愿意引用你的网站(GEO)
开发语言·人工智能·python·聚类·seo·独立站
CCCCCCCCharlie1 小时前
从0到1:一个字节的磁盘之旅
linux·开发语言·c++
比兔代理1 小时前
纯净IP是什么?从IP信誉、黑名单与ASN角度原理解析
开发语言·php
benjiangliu1 小时前
恩培趣味计算机视觉-01-conda环境和Python库
开发语言·python·conda
艾莉丝努力练剑1 小时前
【AI大模型接入SDK】ChatServer接口实现
c++·人工智能·学习·ai·架构·大模型·sdk