一、thread类

1.class thread

class thread用于创建线程。

转自std::thread - cppreference.com

类 thread 表示单个执行线程。线程允许多个函数同时执行。

线程在构造关联的线程对象时立即开始执行(等待任何OS调度延迟),从提供给作为构造函数参数的顶层函数开始。顶层函数的返回值将被忽略,而且若它以抛异常终止,则调用 std::terminate。顶层函数可以通过 std::promise 或通过修改共享变量(可能需要同步,见 std::mutex 与 std::atomic )将其返回值或异常传递给调用方。

std::thread 对象也可能处于不表示任何线程的状态(默认构造、被移动、detach 或 join 后),并且执行线程可能与任何 thread 对象无关(detach 后)。

没有两个 std::thread 对象会表示同一执行线程;std::thread 不是可复制构造 (CopyConstructible) 或可复制赋值 (CopyAssignable) 的,尽管它可移动构造 (MoveConstructible) 且可移动赋值 (MoveAssignable) 。

下面是它最常用的构造函数:

1
2
template< class F, class... Args >
explicit thread( F&& f, Args&&... args );

传入一个函数对象,后面的args代表调用这个函数所需要的参数。

下面写一个简单的程序:

1
2
3
4
5
6
7
#include <iostream>
#include <thread>
int main() {
std::thread func([]()
{ cout << "Hello, world1\n"; });
return 0;
}

如果我们去运行这个函数,会发现并没有输出Hello world!,而是产生了报错,如:terminate called without an active exception。这是因为线程还没有结束的情况下,主程序已经结束了,于是到整个程序结束时,线程还没有输出。如何解决这个问题?很简单,在调用线程后加一个join()成员函数即可,这时主函数就会等待线程结束后再运行,成功输出。

2.join()

1
void join(); // C++11

附:

阻塞当前线程直至 *this 所标识的线程结束其执行。

此时,要有一个疑问:什么是线程?它和主进程的关系是什么?计算机最多可以开辟多少个线程?调用线程会产生什么开销?

这里我们暂不深入解释,留到以后章节细聊,目前先学会简单的使用线程即可。

带传入参数的函数使用thread的方法:

1
2
3
4
std::thread func2([](int a, int b)
{ cout << a + b << '\n'; },
1, 2);
func2.join();

func2接受两个参数,并输出两个参数的和。

附:

this 所标识的线程的完成同步于*对应的从join()成功返回。

*this 自身上不进行同步。同时从多个线程在同一thread对象上调用join()构成数据竞争,导致未定义行为。

3.detach()

detach()成员函数可以分离线程,使线程在后台运行,主线程不再等待它执行完毕,且不会产生报错。

1
2
3
    std::thread func3([]()
{ cout << "Hello, world3!\n"; });
func3.detach();

观察到程序“正常”结束,没有报错,且没有输出"Hello, world3!

附:

从 thread 对象分离执行线程,允许它独立地持续执行。当该线程退出时将释放其分配的任何资源。

调用 detach 后 *this 不再占有任何线程。

4.joinable()

如果一个线程被同时join多次,会产生错误,所以需要一个判断函数来检测该线程是否是活跃的,从而决定它是否可以被join:

1
2
3
4
5
6
7
8
    std::thread func4([]()
{ cout << "Hello, world4!\n"; });
if (func4.joinable()) {
func4.join();
    }
else {
cout << "Thread is not joinable.\n";
}

同时对detach()也适用,可以被detach的线程一定是joinable的,否则会报错std::system_error

附:

检查 std::jthread 对象是否标识活跃的执行线程。具体而言,若 get_id() != std::jthread::id() 则返回 true。故默认构造的 jthread 不可合并。

结束执行代码,但仍未合并的线程仍被当作活跃的执行线程,从而是可合并的。

二、jthread类

在C++20后,加入了可以自动合并和取消的jthread类:

1
2
template< class F, class... Args >
explicit jthread( F&& f, Args&&... args );

使用方法和thread一样,并且无需在手动调用join()函数。

附:

类 jthread 表示单个执行线程。它的一般行为和 std::thread 相同,除了 jthread 在析构时会自动再合并,而且能在特定情况下取消/停止。

线程在关联的线程对象被构造时(在任何操作系统调度延迟后),立即从作为构造函数实参提供的顶层函数开始执行。顶层函数的返回值将被忽略,而若它因抛异常退出,则调用 std::terminate。顶层函数可经由 std::promise,或通过修改共享变量(要求同步,见 std::mutex 与 std::atomic),向调用方交流其返回值或异常。

不同于 std::threadjthread 逻辑上保有一个内部的 std::stop_source 类型私有成员,它维持共享的停止状态。jthread 的构造函数接受一个以 std::stop_token 为其首个实参的函数,jthread 将从其内部的 std::stop_source 传递它。这允许函数在其执行中检查是否已请求停止,而若已请求则返回。

std::jthread 对象亦可处于不表示任何线程的状态(在默认构造、被移动、detach 或 join 后),而执行线程可以不与任何 jthread 对象关联(detach 后)。

两个 std::jthread 对象不可以表示同一执行线程;std::jthread 非可复制构造 (CopyConstructible) 或可复制赋值 (CopyAssignable) ,尽管它为可移动构造 (MoveConstructible) 及可移动赋值 (MoveAssignable) 。

三、本小节最后,我们实现一个“大名鼎鼎”的睡眠排序法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
#include <bits/stdc++.h>

using std::cin;
using std::cout;

int main()
{
std::ios::sync_with_stdio(false);
cin.tie(nullptr), cout.tie(nullptr);
std::vector<int> a(10);
for (int i = 0; i < 10; i++)
{
cin >> a[i];
}
for (int i = 0; i < 10; i++)
{
std::thread([](int a)
{
std::this_thread::sleep_for(std::chrono::milliseconds(a * 10)); // 让每个数字“睡”num * 10 ms
cout << a << ' '; },
a[i])
.detach();
}
std::this_thread::sleep_for(std::chrono::milliseconds(*std::max_element(a.begin(), a.end()) * 10)); // 睡的最长时间为最大的元素 * 10 ms
return 0;
}