C++多线程编程指南

实际代码设计和实现中,常存在多任务并行,因此多线程编程对于一个合格工程师是必备技能,因此准备挖个坑专门来写多线程编程的知识,一是检验自己的学习成果,二是通过自己的分享为他人学习有一点点启发和贡献。

线程是操作系统直接支持的执行单元,所以高级语言通常都是内置线程库;但传统C++(C++98)是没有线程库的,一直到了C++11的是时候才引入了线程库。当前C++线程支持库包含了线程(C++11)、互斥(C++11)、条件变量(C++11)、信号量(C++20)、闩与屏障(C++20)以及future(C++11)的内建支持。(上述标准主要列的首次引入的时间点)

本指南主要还是围绕常用的线程特性介绍,包含以下几个方向内容:

  • 线程 :主要包含std::thread类,同时std::this_thread这个头文件中,可以直接指向当前的线程;
  • 互斥 : 主要包含mutex相关的类,以及用于互斥量管理的lock类;
  • 条件变量 <condition_variable> :主要包含std::condition_variablestd::condition_variabe_any等条件变量相关的类;
  • Future :主要包含std::future类。

展示一个最基本简单的多线程例子:

1
2
3
4
5
6
7
8
9
10
11
12
#include <iostream>
#include <thread>

void thread_task() {
std::cout << "hello thread" << std::endl;
}

int main() {
std::thread t(thread_task);
t.join();
return 0;
}

由于默认的gcc不会加载pthread库,因此需要在编译的时候加上-pthread选项。

线程 <thread>

std::thread

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

编号 构造函数 含义
0 thread() noexcept; 默认构造函数,创建一个空的thread实例
1 thread( thread&& other ) noexcept; 移动构造函数,入参是右值引用,调用移动构造函数之后other不再指向任何线程实例
2 template< class Function, class… Args > explicit thread( Function&& f, Args&&… args ); 初始化构造函数,新线程实例调用f函数,函数参数为args
3 thread( const thread& ) = delete; 拷贝构造函数,但是thread不支持拷贝构造函数,已删除
  • 线程在构造关联的线程对象时立即开始执行(等待任何OS调度延迟),从构造函数入参的函数开始。顶层函数的返回值将被忽略,而且若它以抛异常终止,则调用 std::terminate 。顶层函数可以通过 std::promise 或通过修改共享变量(可能需要同步,见 std::mutex 与 std::atomic )将其返回值或异常传递给调用方。
  • std::thread 对象也可能处于不表示任何线程的状态(默认构造、被移动、 detach 或 join 后),并且执行线程可能与任何 thread 对象无关( detach 后)。
  • 没有两个 std::thread 对象会表示同一执行线程; std::thread 不是可复制构造 (CopyConstructible) 或可复制赋值 (CopyAssignable) 的,尽管它可移动构造 (MoveConstructible) 且可移动赋值 (MoveAssignable) 。
  • 需要注意的是,移动或按值复制线程函数的参数。若需要传递引用参数给线程函数,则必须包装它(例如用 std::ref 或 std::cref )。
    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
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    #include <iostream>
    #include <utility>
    #include <thread>
    #include <chrono>

    void f1(int n)
    {
    for (int i = 0; i < 5; ++i) {
    std::cout << "Thread 1 executing\n";
    ++n;
    std::this_thread::sleep_for(std::chrono::milliseconds(10));
    }
    }

    void f2(int& n)
    {
    for (int i = 0; i < 5; ++i) {
    std::cout << "Thread 2 executing\n";
    ++n;
    std::this_thread::sleep_for(std::chrono::milliseconds(10));
    }
    }

    class foo
    {
    public:
    void bar()
    {
    for (int i = 0; i < 5; ++i) {
    std::cout << "Thread 3 executing\n";
    ++n;
    std::this_thread::sleep_for(std::chrono::milliseconds(10));
    }
    }
    int n = 0;
    };

    class baz
    {
    public:
    void operator()()
    {
    for (int i = 0; i < 5; ++i) {
    std::cout << "Thread 4 executing\n";
    ++n;
    std::this_thread::sleep_for(std::chrono::milliseconds(10));
    }
    }
    int n = 0;
    };

    int main()
    {
    int n = 0;
    foo f;
    baz b;
    std::thread t1; // t1 不是线程
    std::thread t2(f1, n + 1); // 按值传递
    std::thread t3(f2, std::ref(n)); // 按引用传递
    std::thread t4(std::move(t3)); // t4 现在运行 f2() 。 t3 不再是线程
    std::thread t5(&foo::bar, &f); // t5 在对象 f 上运行 foo::bar()
    std::thread t6(std::ref(b)); // t6 在对象 b 上运行 baz::operator()
    t2.join();
    t4.join();
    t5.join();
    t6.join();
    std::cout << "Final value of n is " << n << '\n';
    std::cout << "Final value of foo::n is " << f.n << '\n';
    std::cout << "Final value of baz::n is " << b.n << '\n';
    }

成员函数

  • get_id : 获取当前线程ID
  • joinable :线程是否可被join,joinable概念
  • join : join线程(等待线程执行完成)
  • detach :Detach线程(线程独立运行)
  • swap :交换thread的状态,其实是std::thread对std::swap算法的重载平

互斥量 <mutex>

多线程条件下,不同线程可能会对同一个变量进行操作,由于无法知道哪个线程先操作,哪个线程后操作,最后的结果肯定就是随机的了。这个问题应该就是线程同步问题,为此互斥量mutex就诞生了。
Mutex基本作用就是提供基本的互斥设施,就是某一个时间段只有一个线程可以访问这个互斥设施;
| | Mutex类 | 作用 |
| :— | :— | :— |
| 0 | std::mutex | 提供基本互斥设施 |
| 1 | std::timed_mutex | 提供有时限的互斥设施 |
| 2 | std::recursive_mutex | 提供能被同一线程递归锁定的互斥设施 |
| 3 | std::recursive_timed_mutex | 提供能被同一线程递归锁定并有时限的互斥设施 |
接下来将依次介绍这几个类。

std::mutex

  • 调用方线程从它成功调用 lock 或 try_lock 开始,到它调用 unlock 为止占有 mutex 。
  • 线程占有 mutex 时,所有其他线程若试图要求 mutex 的所有权,则将阻塞(对于 lock 的调用)或收到 false 返回值(对于 try_lock ).
  • 调用方线程在调用 lock 或 try_lock 前必须不占有 mutex 。
    若 mutex 在被线程占有时被销毁,或在占有 mutex 时线程终止,则行为未定义。

    成员函数

  • lock : 锁定互斥设施,如互斥设施不可用则阻塞
  • unlock : 解锁互斥设施
  • try_lock : 尝试锁定互斥设施,若互斥不可用则返回
    std::mutex 既不可复制亦不可移动。

下面是介绍mutex基本用法lock/unlock的荔枝:

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
27
#include <iostream>
#include <chrono>
#include <thread>
#include <mutex>

int g_num = 0; // 为 g_num_mutex 所保护
std::mutex g_num_mutex;

void slow_increment(int id)
{
for (int i = 0; i < 3; ++i) {
g_num_mutex.lock();
++g_num;
std::cout << id << " => " << g_num << '\n';
g_num_mutex.unlock();

std::this_thread::sleep_for(std::chrono::seconds(1));
}
}

int main()
{
std::thread t1(slow_increment, 0);
std::thread t2(slow_increment, 1);
t1.join();
t2.join();
}

下面是std::mutex的try_lock荔枝:

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
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
#include <chrono>
#include <mutex>
#include <thread>
#include <iostream> // std::cout

std::chrono::milliseconds interval(100);

std::mutex mutex;
int job_shared = 0; // 两个线程都能修改 'job_shared',
// mutex 将保护此变量

int job_exclusive = 0; // 只有一个线程能修改 'job_exclusive'
// 不需要保护

// 此线程能修改 'job_shared' 和 'job_exclusive'
void job_1()
{
std::this_thread::sleep_for(interval); // 令 'job_2' 持锁

while (true) {
// 尝试锁定 mutex 以修改 'job_shared'
if (mutex.try_lock()) {
std::cout << "job shared (" << job_shared << ")\n";
mutex.unlock();
return;
} else {
// 不能获取锁以修改 'job_shared'
// 但有其他工作可做
++job_exclusive;
std::cout << "job exclusive (" << job_exclusive << ")\n";
std::this_thread::sleep_for(interval);
}
}
}

// 此线程只能修改 'job_shared'
void job_2()
{
mutex.lock();
std::this_thread::sleep_for(5 * interval);
++job_shared;
mutex.unlock();
}

int main()
{
std::thread thread_1(job_1);
std::thread thread_2(job_2);

thread_1.join();
thread_2.join();
}

std::timed_mutex

类似 mutex 的行为, timed_mutex 提供排他性非递归所有权语义。另外, timed_mutex 提供通过 try_lock_for() 和 try_lock_until() 方法试图带时限地要求 timed_mutex 所有权的能力。

成员函数

  • std::mutex的成员函数相同部分不再介绍
  • try_lock_for : 尝试锁定互斥设施,若互斥在指定时限不可用则返回false
  • try_lock_until : 尝试锁定互斥设施,若直至指定时间互斥不可用则返回false

std::timed_mutex的try_lock_for荔枝:

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
27
28
29
30
31
32
33
34
35
36
37
38
39
40
#include <iostream>
#include <mutex>
#include <thread>
#include <vector>
#include <sstream>

std::mutex cout_mutex; // 控制到 std::cout 的访问
std::timed_mutex mutex;

void job(int id)
{
using Ms = std::chrono::milliseconds;
std::ostringstream stream;

for (int i = 0; i < 3; ++i) {
if (mutex.try_lock_for(Ms(100))) {
stream << "success ";
std::this_thread::sleep_for(Ms(100));
mutex.unlock();
} else {
stream << "failed ";
}
std::this_thread::sleep_for(Ms(100));
}

std::lock_guard<std::mutex> lock(cout_mutex);
std::cout << "[" << id << "] " << stream.str() << "\n";
}

int main()
{
std::vector<std::thread> threads;
for (int i = 0; i < 4; ++i) {
threads.emplace_back(job, i);
}

for (auto& i: threads) {
i.join();
}
}

std::timed_mutex的try_lock_until荔枝:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#include <thread>
#include <iostream>
#include <chrono>
#include <mutex>

std::timed_mutex test_mutex;

void f()
{
auto now=std::chrono::steady_clock::now();
test_mutex.try_lock_until(now + std::chrono::seconds(10));
std::cout << "hello world\n";
}

int main()
{
std::lock_guard<std::timed_mutex> l(test_mutex);
std::thread t(f);
t.join();
}

上面介绍了两个常用mutex类std::mutex和std::timed_mutex,但需要注意的是,实际使用中建议不要直接使用mutex类,而是使用lock类。为了更加便利管理这个Mutex,C++标准线程库提供了实现各类互斥设施所有权的包装器即Lock类;
| | Lock类 | 作用 |
| :— | :— | :— |
| 0 | std::lock_guard | 类 lock_guard 是互斥体包装器,为在作用域块期间占有互斥提供便利 RAII 风格机制 |
| 1 | std::unique_lock | 类 unique_lock 是通用互斥包装器,允许延迟锁定、锁定的有时限尝试、递归锁定、所有权转移和与条件变量一同使用 |

std::lock_guard

std::unique_lock

条件变量 <condition_variable>

Future <future>

参考资料

并发与并行的区别
C++11并发指南系列
cpp reference线程支持库