C++11 多线程(std::thread)详解

线程?进程?多线程?

什么是多线程?

百度百科中的解释:

多线程(multithreading),是指从软件或者硬件上实现多个线程并发执行的技术。具有多线程能力的计算机因有硬件支持而能够在同一时间执行多于一个线程,进而提升整体处理性能。
在一个程序中,这些独立运行的程序片段叫作“线程”(Thread),利用它编程的概念就叫作“多线程处理”。

进程与线程的区别

定义:

进程是正在运行的程序的实例,而线程是是进程中的实际运作单位。

区别:

  • 一个程序有且只有一个进程,但可以拥有至少一个的线程。
  • 不同进程拥有不同的地址空间,互不相关,而不同线程共同拥有相同进程的地址空间。

看了上述介绍,你应该明白进程与线程的区别了。什么,还不明白?下面这幅图应该能让你搞清楚:

(自己画的图,不好看请见谅)

C++11的std::thread

在C中已经有一个叫做pthread的东西来进行多线程编程,但是并不好用 (如果你认为句柄、回调式编程很实用,那请当我没说),所以c++11标准库中出现了一个叫作std::thread的东西。

std::thread常用成员函数

构造&析构函数

常用成员函数

举个栗子

例一:thread的基本使用

// Compiler: MSVC 19.29.30038.1
// C++ Standard: C++17
#include <iostream>
#include <thread>
using namespace std;
void doit() { cout << "World!" << endl; }
int main() {
	// 这里的线程a使用了 C++11标准新增的lambda函数
	// 有关lambda的语法,请参考我之前的一篇博客
	// https://blog.csdn.net/sjc_0910/article/details/109230162
	thread a([]{
		cout << "Hello, " << flush;
	}), b(doit);
	a.join();
	b.join();
	return 0;
}

输出结果:

Hello, World!

或者是

World!
Hello,

那么,为什么会有不同的结果呢?

这就是多线程的特色!

多线程运行时是以异步方式执行的,与我们平时写的同步方式不同。异步方式可以同时执行多条语句。

在上面的例子中,我们定义了2个thread,这2个thread在执行时并不会按照一定的顺序。打个比方,2个thread执行时,就好比赛跑,谁先跑到终点,谁就先执行完毕。

例二:thread执行有参数的函数

// Compiler: MSVC 19.29.30038.1
// C++ Standard: C++17
#include <iostream>
#include <thread>
using namespace std;
void countnumber(int id, unsigned int n) {
	for (unsigned int i = 1; i <= n; i++);
	cout << "Thread " << id << " finished!" << endl;
}
int main() {
	thread th[10];
	for (int i = 0; i < 10; i++)
		th[i] = thread(countnumber, i, 100000000);
	for (int i = 0; i < 10; i++)
		th[i].join();
	return 0;
}

你的输出有可能是这样

Thread 2 finished!Thread 3 finished!
Thread 7 finished!
Thread 5 finished!

Thread 8 finished!
Thread 4 finished!
Thread 6 finished!
Thread 0 finished!
Thread 1 finished!
Thread 9 finished!

注意:我说的是有可能。你的运行结果可能和我的不一样,这是正常现象,在上一个例子中我们分析过原因。

这个例子中我们在创建线程时向函数传递了一些参数,但如果要传递引用参数呢?是不是像这个例子中直接传递就行了?让我们来看看第三个例子:

例三:thread执行带有引用参数的函数

// Compiler: MSVC 19.29.30038.1
// C++ Standard: C++17
#include <iostream>
#include <thread>
using namespace std;
template<class T> void changevalue(T &x, T val) {
	x = val;
}
int main() {
	thread th[100];
	int nums[100];
	for (int i = 0; i < 100; i++)
		th[i] = thread(changevalue<int>, nums[i], i+1);
	for (int i = 0; i < 100; i++) {
		th[i].join();
		cout << nums[i] << endl;
	}
	return 0;
}

如果你尝试编译这个程序,那你的编译器一定会报错

E:\Program Files (x86)\Microsoft Visual Studio\2019\Community\VC\Tools\MSVC\14.29.30037\include\thread(55): error C2672: “std::invoke”: 未找到匹配的重载函数
E:\Program Files (x86)\Microsoft Visual Studio\2019\Community\VC\Tools\MSVC\14.29.30037\include\thread(61): note: 查看对正在编
译的函数 模板 实例化“unsigned int std::thread::_Invoke<_Tuple,0,1,2>(void *) noexcept”的引用
        with
        [
            _Tuple=_Tuple
        ]
E:\Program Files (x86)\Microsoft Visual Studio\2019\Community\VC\Tools\MSVC\14.29.30037\include\thread(66): note: 查看对正在编 
译的函数 模板 实例化“unsigned int (__cdecl *std::thread::_Get_invoke<_Tuple,0,1,2>(std::integer_sequence<size_t,0,1,2>) noexcept)(void *) noexcept”的引用
E:\Program Files (x86)\Microsoft Visual Studio\2019\Community\VC\Tools\MSVC\14.29.30037\include\thread(89): note: 查看对正在编
译的函数 模板 实例化“void std::thread::_Start<void(__cdecl &)(T &,T),int&,_Ty>(_Fn,int &,_Ty &&)”的引用
        with
        [
            T=int,
            _Ty=int,
            _Fn=void (__cdecl &)(int &,int)
        ]
main.cpp(11): note: 查看对正在编译的函数 模板 实例化“std::thread::thread<void(__cdecl &)(T &,T),int&,int,0>(_Fn,int &,int &&)” 
的引用
        with
        [
            T=int,
            _Fn=void (__cdecl &)(int &,int)
        ]
E:\Program Files (x86)\Microsoft Visual Studio\2019\Community\VC\Tools\MSVC\14.29.30037\include\thread(51): error C2893: 未能使
函数模板“unknown-type std::invoke(_Callable &&,_Ty1 &&,_Types2 &&...) noexcept(<expr>)”专用化
E:\Program Files (x86)\Microsoft Visual Studio\2019\Community\VC\Tools\MSVC\14.29.30037\include\type_traits(1589): note: 参见“std::invoke”的声明
E:\Program Files (x86)\Microsoft Visual Studio\2019\Community\VC\Tools\MSVC\14.29.30037\include\thread(51): note: 用下列模板参 
数:
E:\Program Files (x86)\Microsoft Visual Studio\2019\Community\VC\Tools\MSVC\14.29.30037\include\thread(51): note: “_Callable=void (__cdecl *)(T &,T)”
E:\Program Files (x86)\Microsoft Visual Studio\2019\Community\VC\Tools\MSVC\14.29.30037\include\thread(51): note: “_Ty1=int”   
E:\Program Files (x86)\Microsoft Visual Studio\2019\Community\VC\Tools\MSVC\14.29.30037\include\thread(51): note: “_Types2={int}”
E:\Program Files (x86)\Microsoft Visual Studio\2019\Community\VC\Tools\MSVC\14.29.30037\include\thread(51): error C2780: “unknown-type std::invoke(_Callable &&) noexcept(<expr>)”: 应输入 1 个参数,却提供了 3 个
E:\Program Files (x86)\Microsoft Visual Studio\2019\Community\VC\Tools\MSVC\14.29.30037\include\type_traits(1583): note: 参见“std::invoke”的声明

这是怎么回事呢?原来thread在传递参数时,是以右值传递的:

template <class Fn, class... Args>
explicit thread(Fn&& fn, Args&&... args)

划重点:Args&&... args

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

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

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

针对上面的例子,我们可以使用以下代码来修改:

// Compiler: MSVC 19.29.30038.1
// C++ Standard: C++17
#include <iostream>
#include <thread>
using namespace std;
template<class T> void changevalue(T &x, T val) {
	x = val;
}
int main() {
	thread th[100];
	int nums[100];
	for (int i = 0; i < 100; i++)
		th[i] = thread(changevalue<int>, ref(nums[i]), i+1);
	for (int i = 0; i < 100; i++) {
		th[i].join();
		cout << nums[i] << endl;
	}
	return 0;
}

这次编译可以成功通过,你的程序输出的结果应该是这样的:

1
2
3
4
...
99
100

注意事项

  • 线程是在thread对象被定义的时候开始执行的,而不是在调用join函数时才执行的,调用join函数只是阻塞等待线程结束并回收资源。
  • 分离的线程(执行过detach的线程)会在调用它的线程结束或自己结束时释放资源。
  • 线程会在函数运行完毕后自动释放,不推荐利用其他方法强制结束线程,可能会因资源未释放而导致内存泄漏。
  • 没有执行join或detach的线程在程序结束时会引发异常

C++11中的std::atomic和std::mutex

我们现在已经知道如何在c++11中创建线程,那么如果多个线程需要操作同一个变量呢?

为什么要有atomic和mutex

// Compiler: MSVC 19.29.30038.1
// C++ Standard: C++17
#include <iostream>
#include <thread>
using namespace std;
int n = 0;
void count10000() {
	for (int i = 1; i <= 10000; i++)
		n++;
}
int main() {
	thread th[100];
	// 这里偷了一下懒,用了c++11的foreach结构
	for (thread &x : th)
		x = thread(count10000);
	for (thread &x : th)
		x.join();
	cout << n << endl;
	return 0;
}

我的2次输出结果分别是:

991164
996417

我们的输出结果应该是1000000,可是为什么实际输出结果比1000000小呢?

在上文我们分析过多线程的执行顺序——同时进行、无次序,所以这样就会导致一个问题:多个线程进行时,如果它们同时操作同一个变量,那么肯定会出错。为了应对这种情况,c++11中出现了std::atomic和std::mutex。

std::mutex

std::mutex是 C++11 中最基本的互斥量,一个线程将mutex锁住时,其它的线程就不能操作mutex,直到这个线程将mutex解锁。根据这个特性,我们可以修改一下上一个例子中的代码:

例四:std::mutex的使用

// Compiler: MSVC 19.29.30038.1
// C++ Standard: C++17
#include <iostream>
#include <thread>
#include <mutex>
using namespace std;
int n = 0;
mutex mtx;
void count10000() {
	for (int i = 1; i <= 10000; i++) {
		mtx.lock();
		n++;
		mtx.unlock();
	}
}
int main() {
	thread th[100];
	for (thread &x : th)
		x = thread(count10000);
	for (thread &x : th)
		x.join();
	cout << n << endl;
	return 0;
}

执行了好几次,输出结果都是1000000,说明正确。

mutex的常用成员函数

(这里用mutex代指对象

std::atomic

mutex很好地解决了多线程资源争抢的问题,但它也有缺点:太……慢……了……

以例四为标准,我们定义了100个thread,每个thread要循环10000次,每次循环都要加锁、解锁,这样固然会浪费很多的时间,那么该怎么办呢?接下来就是atomic大展拳脚的时间了。

例五:std::atomic的使用

根据atomic的定义,我又修改了例四的代码:

// Compiler: MSVC 19.29.30038.1
// C++ Standard: C++17
#include <iostream>
#include <thread>
// #include <mutex> //这个例子不需要mutex了
#include <atomic>
using namespace std;
atomic_int n = 0;
void count10000() {
	for (int i = 1; i <= 10000; i++) {
		n++;
	}
}
int main() {
	thread th[100];
	for (thread &x : th)
		x = thread(count10000);
	for (thread &x : th)
		x.join();
	cout << n << endl;
	return 0;
}

输出结果:1000000,正常

代码解释

可以看到,我们只是改动了n的类型(int->std::atomic_int),其他的地方一点没动,输出却正常了。

有人可能会问了:这个std::atomic_int是个什么玩意儿?其实,std::atomic_int只是std::atomic<int>的别名罢了。

atomic,本意为原子,官方 (我不确定是不是官方,反正继续解释就对了) 对其的解释是

原子操作是最小的且不可并行化的操作。

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

std::atomic常用成员函数

构造函数

对,atomic没有显式定义析构函数

常用成员函数

atomic能够直接当作普通变量使用,成员函数貌似没啥用,所以这里就不列举了,想搞明白的点这里 (英语渣慎入,不过程序猿中应该没有英语渣吧)

C++11中的std::async

注:std::async定义在 future头文件中。

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

thread可以快速、方便地创建线程,但在async面前,就是小巫见大巫了。

async可以根据情况选择同步执行或创建新线程来异步执行,当然也可以手动选择。对于async的返回值操作也比thread更加方便。

std::async参数

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

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

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

例六:std::async的使用

暂且不管它的返回值std::future是啥,先举个例再说。

// Compiler: MSVC 19.29.30038.1
// C++ Standard: C++17
#include <iostream>
#include <thread>
#include <future>
using namespace std;
int main() {
	async(launch::async, [](const char *message){
		cout << message << flush;
	}, "Hello, ");
	cout << "World!" << endl;
	return 0;
}

你的编译器可能会给出一条警告:

warning C4834: 放弃具有 "nodiscard" 属性的函数的返回值

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

Hello, World!

不过如果你输出的是

World!
Hello,

也别慌,正常现象,多线程嘛!反正我执行了好几次也没出现这个结果。

C++11中的std::future

我们已经知道如何使用async来异步或同步执行任务,但如何获得函数的返回值呢?这时候,async的返回值std::future就派上用场了。

例七:使用std::future获取线程的返回值

在之前的所有例子中,我们创建线程时调用的函数都没有返回值,但如果调用的函数有返回值呢?

// Compiler: MSVC 19.29.30038.1
// C++ Standard: C++17
#include <iostream>
// #include <thread> // 这里我们用async创建线程
#include <future> // std::async std::future
using namespace std;

template<class ... Args> decltype(auto) sum(Args&&... args) {
	// C++17折叠表达式
	// "0 +"避免空参数包错误
	return (0 + ... + args);
}

int main() {
	// 注:这里不能只写函数名sum,必须带模板参数
	future<int> val = async(launch::async, sum<int, int, int>, 1, 10, 100);
	// future::get() 阻塞等待线程结束并获得返回值
	cout << val.get() << endl;
	return 0;
}

输出:

111

代码解释

我们定义了一个函数sum,它可以计算多个数字的和,之后我们又定义了一个对象val,它的类型是std::future<int>,这里的int代表这个函数的返回值是int类型。在创建线程后,我们使用了future::get()来阻塞等待线程结束并获取其返回值。至于sum函数中的折叠表达式(fold expression),不是我们这篇文章的重点。

std::future常用成员函数

构造&析构函数

常用成员函数

std::future_status强枚举类

见上文future::wait_for解释

为啥要有void特化的std::future?

std::future的作用并不只有获取返回值,它还可以检测线程是否已结束、阻塞等待,所以对于返回值是void的线程来说,future也同样重要。

例八:void特化std::future

// Compiler: MSVC 19.29.30038.1
// C++ Standard: C++17
#include <iostream>
#include <future>
using namespace std;
void count_big_number() {
	// C++14标准中,可以在数字中间加上单
	// 引号 ' 来分隔数字,使其可读性更强
	for (int i = 0; i <= 10'0000'0000; i++);
}
int main() {
	future<void> fut = async(launch::async, count_big_number);
	cout << "Please wait" << flush;
	// 每次等待1秒
	while (fut.wait_for(chrono::seconds(1)) != future_status::ready)
		cout << '.' << flush;
	cout << endl << "Finished!" << endl;
	return 0;
}

如果你运行一下这个代码,你也许就能搞懂那些软件的加载画面是怎么实现的。

C++11中的std::promise

在上文,我们已经讲到如何获取async创建线程的返回值。不过在某些特殊情况下,我们可能需要使用thread而不是async,那么如何获得thread的返回值呢?

如果你尝试这么写,那么你的编译器肯定会报错:

std::thread th(func);
std::future<int> return_value = th.join();

还记得之前我们讲的thread成员函数吗?thread::join()的返回值是void类型,所以你不能通过join来获得线程返回值。那么thread里有什么函数能获得返回值呢?

答案是:没有。

惊不惊喜?意不意外?thread竟然不能获取返回值!难道thread真的就没有办法返回点什么东西吗?如果你真是那么想的,那你就太低估C++了。一些聪明的人可能已经想到解决办法了:可以通过传递引用的方式来获取返回值。

例九:引用传递返回值

这个例子中我们先不牵扯多线程的问题。假如你写一个函数,需要返回3个值,那你会怎么办呢?vector?嵌套pair?不不不,都不需要,3个引用参数就可以了。

// Compiler: MSVC 19.29.30038.1
// C++ Standard: C++17
#include <iostream>
using namespace std;
constexpr long double PI = 3.14159265358979323846264338327950288419716939937510582097494459230781640628;
// 给定圆的半径r,求圆的直径、周长及面积
void get_circle_info(double r, double &d, double &c, double &s) {
	d = r * 2;
	c = PI * d;
	s = PI * r * r;
}
int main() {
	double r;
	cin >> r;
	double d, c, s;
	get_circle_info(r, d, c, s);
	cout << d << ' ' << c << ' ' <<  s << endl;
	return 0;
}

输入5,输出:

10 31.4159 78.5398

如果你和我输出有一些误差,是正常现象,不同编译器、不同机器处理精度也有所不同

std::promise到底是啥

promise实际上是std::future的一个包装,在讲解future时,我们并没有牵扯到改变future值的问题,但是如果使用thread以引用传递返回值的话,就必须要改变future的值,那么该怎么办呢?

实际上,future的值不能被改变,但你可以通过promise来创建一个拥有特定值的future。什么?没听懂?好吧,那我就举个例子:

例十:std::future的值不能改变,那么如何利用引用传递返回值

constexpr int a = 1;

现在,把常量当成future,把a当作一个future对象,那我们想拥有一个值为2的future对象该怎么办?
很简单:

constexpr int a = 1;
constexpr int b = 2;

这样,我们就不用思考如何改动a的值,直接创建一个新常量就能解决问题了。
promise的原理就是这样,不改变已有future的值,而是创建新的future对象。什么?还没听懂?好吧,记住这句话:

future的值不能改变,promise的值可以改变。

std::promise常用成员函数

构造&析构函数

常用成员函数

例十一:std::promise的使用

以例七中的代码为基础加以修改:

// Compiler: MSVC 19.29.30038.1
// C++ Standard: C++17
#include <iostream>
#include <thread>
#include <future> // std::promise std::future
using namespace std;

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

template<class ... Args> void sum_thread(promise<long long> &val, Args&&... args) {
	val.set_value(sum(args...));
}

int main() {
	promise<long long> sum_value;
	thread get_sum(sum_thread<int, int, int>, ref(sum_value), 1, 10, 100);
	cout << sum_value.get_future().get() << endl;
	get_sum.join(); // 感谢评论区 未来想做游戏 的提醒
	return 0;
}

输出:

111

C++11中的std::this_thread

上面讲了那么多关于创建、控制线程的方法,现在该讲讲关于线程控制自己的方法了。

在<thread>头文件中,不仅有std::thread这个类,而且还有一个std::this_thread命名空间,它可以很方便地让线程对自己进行控制。

std::this_thread常用函数

std::this_thread是个命名空间,所以你可以使用 using namespace std::this_thread;这样的语句来展开这个命名空间,不过我不建议这么做。

例十二:std::this_thread中常用函数的使用

#include <iostream>
#include <thread>
using namespace std;
atomic_bool ready = 0;
// uintmax_t ==> unsigned long long
void sleep(uintmax_t ms) {
	this_thread::sleep_for(chrono::milliseconds(ms));
}
void count() {
	while (!ready) this_thread::yield();
	for (int i = 0; i <= 20'0000'0000; i++);
	cout << "Thread " << this_thread::get_id() << " finished!" << endl;
	return;
}
int main() {
	thread th[10];
	for (int i = 0; i < 10; i++)
		th[i] = thread(::count);
	sleep(5000);
	ready = true;
	cout << "Start!" << endl;
	for (int i = 0; i < 10; i++)
		th[i].join();
	return 0;
}

我的输出:

Start!
Thread 8820 finished!Thread 6676 finished!

Thread 13720 finished!
Thread 3148 finished!
Thread 13716 finished!
Thread 16424 finished!
Thread 14228 finished!
Thread 15464 finished!
Thread 3348 finished!
Thread 6804 finished!

你的输出几乎不可能和我一样,不仅是多线程并行的问题,而且每个线程的id也可能不同。

结尾

这篇文章到这里就结束了 (说不定以后还会写个c++20的std::jthread讲解)。这是我第一篇接近2万字的文章。其实我刚开始写这篇文章时,也没想到这篇文章会吸引这么多人看,评论里还会有很多的好评,并且还上过一次热榜:

(厚颜无耻地给自己点赞)

又入选过C/C++领域内容榜:

发布于 2022-07-11 15:31

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值