注:此教程以 Visual Studio 2019 Version 16.10.3 (MSVC 19.29.30038.1) 为标准,大多数内容参照cplusplus.com里的解释
此文章允许转载,但请标明出处(https://blog.csdn.net/sjc_0910/article/details/118861539)
前方高能:本文字数接近2万
百度百科中的解释:
多线程(multithreading),是指从软件或者硬件上实现多个线程并发执行的技术。具有多线程能力的计算机因有硬件支持而能够在同一时间执行多于一个线程,进而提升整体处理性能。
在一个程序中,这些独立运行的程序片段叫作“线程”(Thread),利用它编程的概念就叫作“多线程处理”。
定义:
进程是正在运行的程序的实例,而线程是是进程中的实际运作单位。
区别:
看了上述介绍,你应该明白进程与线程的区别了。什么,还不明白?下面这幅图应该能让你搞清楚:
(自己画的图,不好看请见谅)
在C中已经有一个叫做pthread的东西来进行多线程编程,但是并不好用 (如果你认为句柄、回调式编程很实用,那请当我没说),所以c++11标准库中出现了一个叫作std::thread的东西。
函数 | 类别 | 作用 |
---|---|---|
thread() noexcept | 默认构造函数 | 创建一个线程, 什么也不做 |
template 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() 结果程序) |
// Compiler: MSVC 19.29.30038.1
// C++ Standard: C++17
#include
#include
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执行时,就好比赛跑,谁先跑到终点,谁就先执行完毕。
// Compiler: MSVC 19.29.30038.1
// C++ Standard: C++17
#include
#include
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!
注意:我说的是有可能。你的运行结果可能和我的不一样,这是正常现象,在上一个例子中我们分析过原因。
这个例子中我们在创建线程时向函数传递了一些参数,但如果要传递引用参数呢?是不是像这个例子中直接传递就行了?让我们来看看第三个例子:
// Compiler: MSVC 19.29.30038.1
// C++ Standard: C++17
#include
#include
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) 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(_Fn,int &,_Ty &&)”的引用
with
[
T=int,
_Ty=int,
_Fn=void (__cdecl &)(int &,int)
]
main.cpp(11): note: 查看对正在编译的函数 模板 实例化“std::thread::thread(_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()”专用化
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()”: 应输入 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
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
#include
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
(中间省略了一堆数)
join
或detach
的线程在程序结束时会引发异常我们现在已经知道如何在c++11中创建线程,那么如果多个线程需要操作同一个变量呢?
// Compiler: MSVC 19.29.30038.1
// C++ Standard: C++17
#include
#include
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
是 C++11 中最基本的互斥量,一个线程将mutex锁住时,其它的线程就不能操作mutex,直到这个线程将mutex解锁。根据这个特性,我们可以修改一下上一个例子中的代码:
// Compiler: MSVC 19.29.30038.1
// C++ Standard: C++17
#include
#include
#include
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
代指对象
)
函数 | 作用 |
---|---|
void lock() | 将mutex上锁。 如果mutex已经被其它线程上锁, 那么会阻塞,直到解锁; 如果mutex已经被同一个线程锁住, 那么会产生死锁。 |
void unlock() | 解锁mutex,释放其所有权。 如果有线程因为调用lock()不能上锁而被阻塞,则调用此函数会将mutex的主动权随机交给其中一个线程; 如果mutex不是被此线程上锁,那么会引发未定义的异常。 |
bool try_lock() | 尝试将mutex上锁。 如果mutex未被上锁,则将其上锁并返回true; 如果mutex已被锁则返回false。 |
mutex很好地解决了多线程资源争抢的问题,但它也有缺点:太……慢……了……
以例四为标准,我们定义了100个thread,每个thread要循环10000次,每次循环都要加锁、解锁,这样固然会浪费很多的时间,那么该怎么办呢?接下来就是atomic大展拳脚的时间了。
根据atomic的定义,我又修改了例四的代码:
// Compiler: MSVC 19.29.30038.1
// C++ Standard: C++17
#include
#include
// #include //这个例子不需要mutex了
#include
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
的别名罢了。
atomic,本意为原子,官方 (我不确定是不是官方,反正继续解释就对了) 对其的解释是
原子操作是最小的且不可并行化的操作。
这就意味着即使是多线程,也要像同步进行一样同步操作atomic对象,从而省去了mutex上锁、解锁的时间消耗。
对,atomic没有显式定义析构函数
函数 | 类型 | 作用 |
---|---|---|
atomic() noexcept = default | 默认构造函数 | 构造一个atomic对象(未初始化,可通过atomic_init进行初始化) |
constexpr atomic(T val) noexcept | 初始化构造函数 | 构造一个atomic对象,用val 的值来初始化 |
atomic(const atomic&) = delete | 复制构造函数 | (已删除) |
atomic能够直接当作普通变量使用,成员函数貌似没啥用,所以这里就不列举了,想搞明白的点这里 (英语渣慎入,不过程序猿中应该没有英语渣吧)
注:std::async定义在
future
头文件中。
thread可以快速、方便地创建线程,但在async面前,就是小巫见大巫了。
async可以根据情况选择同步执行或创建新线程来异步执行,当然也可以手动选择。对于async的返回值操作也比thread更加方便。
不同于thread,async是一个函数,所以没有成员函数。
重载版本 | 作用 |
---|---|
template future async (Fn&& fn, Args&&… args) |
异步或同步(根据操作系统而定)以args为参数执行fn 同样地,传递引用参数需要 std::ref 或std::cref |
template future async (launch policy, Fn&& fn, Args&&… args); |
异步或同步(根据policy 参数而定(见下文))以args为参数执行fn,引用参数同上 |
std::launch有2个枚举值和1个特殊值:
标识符 | 实际值(以Visual Studio 2019为标准) | 作用 |
---|---|---|
枚举值:launch::async | 0x1(1) | 异步启动 |
枚举值:launch::deferred | 0x2(2) | 在调用future::get、future::wait时同步启动(std::future见后文) |
特殊值:launch::async | launch::defereed | 0x3(3) | 同步或异步,根据操作系统而定 |
暂且不管它的返回值std::future是啥,先举个例再说。
// Compiler: MSVC 19.29.30038.1
// C++ Standard: C++17
#include
#include
#include
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,
也别慌,正常现象,多线程嘛!反正我执行了好几次也没出现这个结果。
我们已经知道如何使用async来异步或同步执行任务,但如何获得函数的返回值呢?这时候,async的返回值std::future就派上用场了。
在之前的所有例子中,我们创建线程时调用的函数都没有返回值,但如果调用的函数有返回值呢?
// Compiler: MSVC 19.29.30038.1
// C++ Standard: C++17
#include
// #include // 这里我们用async创建线程
#include // 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类型。在创建线程后,我们使用了future::get()来阻塞等待线程结束并获取其返回值。至于sum函数中的折叠表达式(fold expression),不是我们这篇文章的重点。
函数 | 类型 | 作用 |
---|---|---|
future() noexcept | 默认构造函数 | 构造一个空的、无效的future对象,但可以移动分配到另一个future对象 |
future(const future&) = delete | 复制构造函数 | (已删除) |
future (future&& x) noexcept | 移动构造函数 | 构造一个与x 相同的对象并破坏x |
~future() | 析构函数 | 析构对象 |
函数 | 作用 |
---|---|
一般:T get() 当类型为引用:R& future 当类型为void:void future::get() |
阻塞等待线程结束并获取返回值。 若类型为void,则与 future::wait() 相同。只能调用一次。 |
void wait() const | 阻塞等待线程结束 |
template future_status wait_for(const chrono::duration |
阻塞等待rel_time (rel_time 是一段时间),若在这段时间内线程结束则返回 future_status::ready 若没结束则返回 future_status::timeout 若async是以 launch::deferred 启动的,则不会阻塞并立即返回future_status::deferred |
不知道std::chrono::duration的点这里
见上文future::wait_for
解释
std::future的作用并不只有获取返回值,它还可以检测线程是否已结束、阻塞等待,所以对于返回值是void的线程来说,future也同样重要。
// Compiler: MSVC 19.29.30038.1
// C++ Standard: C++17
#include
#include
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;
}
如果你运行一下这个代码,你也许就能搞懂那些软件的加载画面是怎么实现的。
在上文,我们已经讲到如何获取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
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
如果你和我输出有一些误差,是正常现象,不同编译器、不同机器处理精度也有所不同
promise实际上是std::future的一个包装,在讲解future时,我们并没有牵扯到改变future值的问题,但是如果使用thread以引用传递返回值的话,就必须要改变future的值,那么该怎么办呢?
实际上,future的值不能被改变,但你可以通过promise来创建一个拥有特定值的future。什么?没听懂?好吧,那我就举个例子:
constexpr int a = 1;
现在,把常量当成future,把a当作一个future对象,那我们想拥有一个值为2的future对象该怎么办?
很简单:
constexpr int a = 1;
constexpr int b = 2;
这样,我们就不用思考如何改动a的值,直接创建一个新常量就能解决问题了。
promise的原理就是这样,不改变已有future的值,而是创建新的future对象。什么?还没听懂?好吧,记住这句话:
future的值不能改变,promise的值可以改变。
函数 | 类型 | 作用 |
---|---|---|
promise() | 默认构造函数 | 构造一个空的promise对象 |
template |
构造函数 | 与默认构造函数相同,但使用特定的内存分配器alloc 构造对象 |
promise (const promise&) = delete | 复制构造函数 | (已删除) |
promise (promise&& x) noexcept | 移动构造函数 | 构造一个与x 相同的对象并破坏x |
~promise() | 析构函数 | 析构对象 |
函数 | 作用 |
---|---|
一般: void set_value (const T& val) void set_value (T&& val) 当类型为引用:void promise 当类型为void:void promise::set_value (void) |
设置promise的值并将共享状态设为ready(将future_status设为ready) void特化:只将共享状态设为ready |
future get_future() | 构造一个future对象,其值与promise相同,status也与promise相同 |
以例七中的代码为基础加以修改:
// Compiler: MSVC 19.29.30038.1
// C++ Standard: C++17
#include
#include
#include // 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;
return 0;
}
输出:
111
上面讲了那么多关于创建、控制线程的方法,现在该讲讲关于线程控制自己的方法了。
在
头文件中,不仅有std::thread这个类,而且还有一个std::this_thread命名空间,它可以很方便地让线程对自己进行控制。
std::this_thread是个命名空间,所以你可以使用
using namespace std::this_thread;
这样的语句来展开这个命名空间,不过我不建议这么做。
函数 | 作用 |
---|---|
std::thread::id get_id() noexcept | 获取当前线程id |
template void sleep_for( const std::chrono::duration |
等待sleep_duration (sleep_duration 是一段时间) |
void yield() noexcept | 暂时放弃线程的执行,将主动权交给其他线程 (放心,主动权还会回来) |
#include
#include
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++领域内容榜:
这着实是出乎我的意料的。在此也感谢评论区里各位的好评,我就不一一回复了。
如果你觉得这篇文章有不对、不标准之处,也可以在评论区里说一下,感谢支持。