C++11 多线程

C++11 多线程

参考:

C++11 thread多线程:https://blog.csdn.net/sjc_0910/article/details/118861539

C++11 左右值引用:https://blog.csdn.net/Jacky_Feng/article/details/120742414

C++11 移动构造函数:https://zhuanlan.zhihu.com/p/365412262

C++11 std::thread

在C中已经有一个叫做pthread的东西来进行多线程编程,但是并不好用 ,所以c++11标准库中出现了一个叫作std::thread的东西。

1 成员函数

构造函数和析构函数

函数 类别 作用
thread() noexcept 默认构造函数 创建一个线程, 什么也不做
template <class Fn, class… Args> explicit thread(Fn&& fn, Args&&… args) 初始化构造函数 创建一个线程, 以args为参数 执行fn函数
thread(const thread&) = delete 复制构造函数 (已删除)
thread(thread&& x) noexcept 移动构造函数 构造一个与x 相同的对象,会破坏x对象
~thread() 析构函数 析构对象

成员函数

函数 作用
void join() 等待子线程结束并清理资源(会阻塞)
bool joinable() 返回线程是否可以执行join函数
void detach() 将线程与调用其的线程分离,彼此独立执行(此函数必须在线程创建时立即调用,且调用此函数会使其不能被join)
std::thread::id get_id() 获取线程id
thread& operator=(thread &&rhs) 见移动构造函数(如果对象是joinable的,那么会调用std::terminate()结果程序)

2 示例

  • lambda表达式传参
#include<iostream>
#include<thread>

void func() {
	std::cout << "HiHiHi";
}
int main() {
	std::thread td1([] {
		std::cout << "Hello,";
	}), td2([] {
		std::cout << "World!";
	}), td3(func);

	td1.join();
	td2.join();
	td3.join();
	return 0;
}

多线程运行时是以异步方式执行的,异步方式可以同时执行多条语句。

在上面的例子中,我们定义了3个thread,这3个thread在执行时并不会按照一定的顺序。

输出可能以任意一个顺序输出。

  • 传入的函数带参数
#include<iostream>
#include<thread>

void func(int a, int b) {
	std::cout << "Answer: " << a + b;
}
int main() {
	std::thread td3(func, 1, 2);

	td3.join();
	return 0;
}
  • 执行带引用参数的函数

注意观察构造函数:Args&&... args

很明显的右值引用,那么我们该如何传递一个左值呢?std::refstd::cref很好地解决了这个问题。

std::ref 可以包装按引用传递的值。

std::cref 可以包装按const引用传递的值。

#include<iostream>
#include<thread>

template<class T>
void changeValue(T& x, T val) {
	x = val;
}

int main() {
	std::thread t[20];
	int nums[20];

	// 调用构造函数
	for(int i = 0; i < 20; i++) {
		t[i] = std::thread(changeValue<int>, std::ref(nums[i]), i + 1);
	}

	for(int i = 0; i < 20; i++) {
		t[i].join();
		std::cout << nums[i] << "\n";
	}
	return 0;
}

3 注意

  • 线程是在thread对象被定义的时候开始执行的,而不是在调用join函数时才执行的,调用join函数只是阻塞等待线程结束并回收资源。

join函数是等待子线程完成,然后主线程再继续执行

  • 分离的线程(执行过detach的线程)会在调用它的线程结束或自己结束时释放资源。
  • 线程会在函数运行完毕后自动释放,不推荐利用其他方法强制结束线程,可能会因资源未释放而导致内存泄漏。
  • 没有执行joindetach的线程在程序结束时会引发异常

C++11 std::mutex与std::atomic

1 std::mutex

多线程操作同一变量

#include<iostream>
#include<thread>

int val = 0;
void increase() {
	for(int i = 1; i <= 100000; i++) {
		val++;
	}
}

int main() {
	std::thread t[20];

	for(int i = 0; i < 20; i++) {
		t[i] = std::thread(increase);
	}
	for(int i = 0; i < 20; i++) {
		t[i].join();
	}
	std::cout << val << "\n";	
	return 0;
}

可以发现此代码输出并不是正常情况下的2000000,原因就是多线程访问并修改一个变量时造成了冲突,所以mutex派上用场

函数 作用
void lock() 将mutex上锁。
如果mutex已经被其它线程上锁, 那么会阻塞,直到解锁;
如果mutex已经被同一个线程锁住, 那么会产生死锁。
void unlock() 解锁mutex,释放其所有权。 如果有线程因为调用lock()不能上锁而被阻塞,则调用此函数会将mutex的主动权随机交给其中一个线程; 如果mutex不是被此线程上锁,那么会引发未定义的异常。
bool try_lock() 尝试将mutex上锁。 如果mutex未被上锁,则将其上锁并返回true;
如果mutex已被锁则返回false。

可以利用mutex进行改进:

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

std::mutex mtx;
int val = 0;

void increase() {
	for(int i = 1; i <= 100000; i++) {
		mtx.lock();
		val++;
		mtx.unlock();
	}
}

int main() {
	std::thread t[20];

	for(int i = 0; i < 20; i++) {
		t[i] = std::thread(increase);
	}
	for(int i = 0; i < 20; i++) {
		t[i].join();
	}
	std::cout << val << "\n";
	return 0;
}

2 std::atomic

mutex每次循环都要加锁解锁,比较慢,而atomic比较快。将上述代码改成下面代码同样也可。

#include<iostream>
#include<thread>
#include<atomic>

std::atomic_int val = 0;

void increase() {
	for(int i = 1; i <= 100000; i++) {
		val++;
	}
}

int main() {
	std::thread t[20];

	for(int i = 0; i < 20; i++) {
		t[i] = std::thread(increase);
	}
	for(int i = 0; i < 20; i++) {
		t[i].join();
	}
	std::cout << val << "\n";
	return 0;
}

std::atomic_int只是std::atomic<int>的别名。

即使是多线程,也要像同步进行一样同步操作atomic对象,从而省去了mutex上锁、解锁的时间消耗。

构造函数 类型 作用
atomic() noexcept = default 默认构造函数 构造一个atomic对象(未初始化,可通过atomic_init进行初始化)
constexpr atomic(T val) noexcept 初始化构造函数 构造一个atomic对象,用val的值来初始化

C++11 std::async

1 async

头文件<future>

大多数情况下使用async而不用thread

  • thread可以快速、方便地创建线程,但在async面前,就是小巫见大巫了。
  • async可以根据情况选择同步执行或创建新线程来异步执行,当然也可以手动选择。对于async的返回值操作也比thread更加方便。

std::async参数:

不同于thread,async是一个函数,所以没有成员函数。

重载版本 作用
template <class Fn, class… Args>
future<typename result_of<Fn(Args…)>::type>
async (Fn&& fn, Args&&… args)
异步或同步(根据操作系统而定)以args为参数执行fn
同样地,传递引用参数需要std::refstd::cref
template <class Fn, class… Args>
future<typename result_of<Fn(Args…)>::type>
async (launch policy, Fn&& fn, Args&&… args);
异步或同步(根据policy参数而定(见下文))以args为参数执行fn,引用参数同上

2 std::launch强枚举类(enum class)

std::launch有2个枚举值和1个特殊值:

标识符 实际值 作用
枚举值:launch::async 0x1(1) 异步启动
枚举值:launch::deferred 0x2(2) 在调用future::getfuture::wait时同步启动(std::future见后文)
特殊值:launch::async | launch::deferred 0x3(3) 同步或异步,根据操作系统而定
#include<iostream>
#include<thread>
#include<future>

int main() {
	std::async(std::launch::async, [](const char* message) {
		std::cout << message;
	}, "Hello, ");
	std::cout << "World\n";
	return 0;
}

编译器可能会有警告,因为编译器不想让你丢弃async的返回值std::future,不过在这个例子中不需要它,忽略这个警告就行了。

3 std::future

使用它获取函数返回值

#include<iostream>
#include<thread>
#include<future>

template<class... Args>
decltype(auto) sum(Args&&... args) {
	return (0 + ... + args);
}

int main() {
	std::future<int> val = std::async(std::launch::async, sum<int, int, int>, 1, 2, 3);
	std::cout << val.get() << "\n";
	return 0;
}

输出

6

项目练习

附上我以此为基的学习项目:

基于C++17的线程池:https://github.com/anda522/ThreadPool


   转载规则


《C++11 多线程》 行码棋 采用 知识共享署名 4.0 国际许可协议 进行许可。
 上一篇
Pandas数据处理 Pandas数据处理
Pandas 读取数据 import pandas as pd df = pd.read_csv(path) # path为读取数据的路径 查看数据 >>> import numpy as np >>&g
2023-03-24 2024-02-20
下一篇 
Latex语法 Latex语法
Latex语法 在begin{document}和end{document}之间的就是正文区,而在这之前的就是导言区。 1 宏包导入 \usepackage{宏包1, 宏包2} 2 中文支持 无论是在线工具还是本地工具,LaTeX默认
2023-03-09 2024-02-20
  目录