C++ 从入门到精通之十六

大纲

C++ 并发编程

std::async

这里不再介绍 std::async 的基础使用,而是重点介绍 std::async 进阶使用的内容。std::async 支持以下三种启动策略:

启动策略说明
std::launch::async立即执行,创建新线程执行任务。
std::launch::deferred延迟执行,直到调用 get() 或 wait() 时才在当前线程(不会创建新线程)执行。
默认策略由标准库实现决定采用立即执行还是延迟执行。

std::launch::async

std::launch::async 启动策略会强制异步任务在新的线程中执行,而不是延迟到获取结果时才执行。同时,与该异步任务关联的 std::future 在销毁时,会确保异步任务执行完成,因此即使主线程没有主动调用 std::future 的 get() 或 wait() 函数,程序结束前仍需要等待异步任务执行完成。

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

int process(const int milliseconds) {
std::cout << "process() start, current thread id " << std::this_thread::get_id() << std::endl;

// 模拟业务处理耗时
const std::chrono::milliseconds ms(milliseconds);
std::this_thread::sleep_for(ms);

std::cout << "process() end, current thread id " << std::this_thread::get_id() << std::endl;

// 返回业务处理结果
return 5;
}

int main() {
std::cout << "main() run, thread id " << std::this_thread::get_id() << std::endl;

// 第一个参数是启动策略,第二个参数是线程函数,第三个参数是线程函数的参数
std::future<int> result = std::async(std::launch::async, process, 5000);

std::cout << "continue ..." << std::endl;

// 使用 std::launch::async 执行策略后,会立即创建子线程执行任务,主线程调用 get() 后会阻塞等待任务执行完成并获取执行结果
// 这里即使不手动调用 get() 或者 wait(),主线程也会等待子线程执行完成后再结束
const int num = result.get();
std::cout << "num = " << num << std::endl;

std::cout << "main() end, thread id " << std::this_thread::get_id() << std::endl;

return 0;
}

程序运行的结果如下:

1
2
3
4
5
6
main() run, thread id 1
continue ...
process() start, current thread id 2
process() end, current thread id 2
num = 5
main() end, thread id 1

std::launch::deferred

std::thread 创建线程后,如果系统资源紧张,线程创建可能失败并抛出异常;如果异常未被妥善处理,可能进一步导致整个程序异常退出。因此,在使用 std::thread 时需要额外考虑线程创建失败的情况。相比之下,std::async 在某些情况下可以根据运行时策略(启动策略)决定是否创建新的线程,从而避免每次调用都显式创建线程。比如,当 std::async 使用 std::launch::deferred 启动策略时,会延迟执行任务,直到调用 std::future 的 get() 或 wait() 时才在当前线程(不会创建新线程)执行任务。

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

int process(const int milliseconds) {
std::cout << "process() start, current thread id " << std::this_thread::get_id() << std::endl;

// 模拟业务处理耗时
const std::chrono::milliseconds ms(milliseconds);
std::this_thread::sleep_for(ms);

std::cout << "process() end, current thread id " << std::this_thread::get_id() << std::endl;

// 返回业务处理结果
return 5;
}

int main() {
std::cout << "main() run, thread id " << std::this_thread::get_id() << std::endl;

// 第一个参数是启动策略,第二个参数是线程函数,第三个参数是线程函数的参数
std::future<int> result = std::async(std::launch::deferred, process, 5000);

std::cout << "continue ..." << std::endl;

// 使用 std::launch::deferred 执行策略后,不会创建新线程,当 get() 被调用后才会在当前主线程(非子线程)开始执行任务
// 如果 get() 或者 wait() 不被调用,那么任务永远不会执行
const int num = result.get();
std::cout << "num = " << num << std::endl;

std::cout << "main() end, thread id " << std::this_thread::get_id() << std::endl;

return 0;
}

程序运行的结果如下:

1
2
3
4
5
6
main() run, thread id 1
continue ...
process() start, current thread id 1
process() end, current thread id 1
num = 5
main() end, thread id 1

自行选择启动策略

std::launch::async | std::launch::deferred 表示两种启动策略都允许使用,由标准库实现自行选择其中一种。因此,最终启动策略不是固定的。具体来说:

  • 如果选择 async:任务会立即异步执行,通常在新线程中执行。
  • 如果选择 deferred:任务不会立即执行,而是在关联的 std::future 上调用 get() 或 wait() 等等待操作时才执行,并且执行线程通常就是调用等待操作的线程(不会创建新线程)。

总结

当使用 std::launch::async | std::launch::deferred 时,表示允许标准库在异步执行和延迟执行之间进行选择,最终采用哪一种启动策略由标准库实现决定,程序不能假定一定会创建新的线程。这也是 std::async 默认启动策略的行为。

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

int process(const int milliseconds) {
std::cout << "process() start, current thread id " << std::this_thread::get_id() << std::endl;

// 模拟业务处理耗时
const std::chrono::milliseconds ms(milliseconds);
std::this_thread::sleep_for(ms);

std::cout << "process() end, current thread id " << std::this_thread::get_id() << std::endl;

// 返回业务处理结果
return 5;
}

int main() {
std::cout << "main() run, thread id " << std::this_thread::get_id() << std::endl;

// 第一个参数是启动策略,第二个参数是线程函数,第三个参数是线程函数的参数
std::future<int> result = std::async(std::launch::async | std::launch::deferred, process, 5000);

std::cout << "continue ..." << std::endl;

// 上面的两种启动策略任意选择一种(选择的结果是不确定的,由标准库实现自行选择)
const int num = result.get();
std::cout << "num = " << num << std::endl;

std::cout << "main() end, thread id " << std::this_thread::get_id() << std::endl;

return 0;
}

程序运行的结果如下:

1
2
3
4
5
6
main() run, thread id 1
continue ...
process() start, current thread id 2
process() end, current thread id 2
num = 5
main() end, thread id 1

判断使用的启动策略

在 C++ 中,可以使用 std::future 的 wait_for() 来判断 std::async 使用的是哪种启动策略(std::launch::async 或者 std::launch::deferred)。

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

int process(const int milliseconds) {
std::cout << "process() start, current thread id " << std::this_thread::get_id() << std::endl;

// 模拟业务处理耗时
const std::chrono::milliseconds ms(milliseconds);
std::this_thread::sleep_for(ms);

std::cout << "process() end, current thread id " << std::this_thread::get_id() << std::endl;

// 返回业务处理结果
return 5;
}

int main() {
std::cout << "main() run, thread id " << std::this_thread::get_id() << std::endl;

// 第一个参数是启动策略,第二个参数是线程函数,第三个参数是线程函数的参数
std::future<int> result = std::async(std::launch::async | std::launch::deferred, process, 5000);

// 判断 std::async 使用哪种启动策略
const std::future_status status = result.wait_for(std::chrono::seconds(0));
if ( status == std::future_status::deferred ) {
// 延迟执行策略
std::cout << "thread deferred" << std::endl;
const int num = result.get();
std::cout << "result = " << num << std::endl;
} else {
// 异步执行策略
if (status == std::future_status::ready) {
// 线程执行完成并返回结果
std::cout << "thread finish" << std::endl;
const int num = result.get();
std::cout << "result = " << num << std::endl;
}
else {
// 线程执行超时
std::cout << "thread timeout" << std::endl;
const int num = result.get();
std::cout << "result = " << num << std::endl;
}
}

std::cout << "main() end, thread id " << std::this_thread::get_id() << std::endl;

return 0;
}

程序运行的结果如下:

1
2
3
4
5
6
main() run, thread id 1
thread timeout
process() start, current thread id 2
process() end, current thread id 2
result = 5
main() end, thread id 1

特别注意

  • std::future::wait_for() 可以用于在指定时间内等待异步任务完成,并通过返回值 std::future_status 判断任务当前状态。需要注意的是,wait_for() 本身不会影响异步任务的执行,也不会取消任务,只是用于检测任务是否在指定时间内完成。
  • 当使用 std::async(std::launch::async, ...) 创建异步任务时,即使 wait_for() 返回 timeout,表示任务仍在执行,异步线程也会继续运行。若对应的 std::future 对象生命周期结束,在析构阶段会等待异步任务执行完成后再退出程序。因此,wait_for() 适用于超时检测、任务状态轮询等场景,但不能用于终止正在执行的异步任务。

与 std::thread 的区别

std::thread 和 std::async 都可以用于 C++ 的异步执行,但两者的设计目标不同:std::thread 更偏向于直接管理线程,而 std::async 更偏向于管理异步任务及其执行结果。

对比项std::threadstd::async
核心定位线程管理异步任务管理
返回值不直接提供返回值可以通过 std::future 获取返回值
异常处理线程内部异常不能直接传递给创建线程异常会保存到 std::future,获取结果时可以重新抛出异常
线程创建通常直接创建一个新的线程可以选择异步执行,也可以延迟执行
启动策略创建线程后立即开始执行支持 std::launch::async、std::launch::deferred 执行策略
生命周期管理需要手动 join() 或 detach()通过 std::future 管理任务结果和等待
资源不足创建线程失败时可能抛出异常使用 std::launch::async 执行策略时,线程资源不足同样可能导致任务启动失败
适用场景需要精细控制线程更关注任务执行结果

1、std::thread 更关注「线程」

std::thread 的核心是创建和管理一个线程。使用 std::thread 时,需要关注线程的生命周期,例如:

  • 线程什么时候创建;
  • 线程什么时候结束;
  • 是否需要 join();
  • 是否需要 detach();
  • 多线程之间如何同步和通信。

因此,std::thread 更适合需要对线程本身进行控制的场景。

2、std::async 更关注「任务」

  • std::async 的核心是异步执行一个任务,并获得任务的执行结果。
  • std::async 通常与 std::future 配合使用:
    • 提交任务 → 异步执行 → 返回 std::future → 通过 future 获取结果。
  • 因此,如果一个函数有返回值,并且希望在后台执行完成后获取这个结果,std::async 通常比 std::thread 更方便。

3、std::async 可以控制任务的执行策略

  • std::async 可以指定:
    • std::launch::async:要求异步执行任务,通常由新的线程执行。
    • std::launch::deferred:延迟执行任务,直到需要获取结果时才执行,并且在调用 get() 或 wait() 的线程中执行。
    • std::launch::async | std::launch::deferred:由标准库实现自行选择执行策略。
  • std::thread 本身没有这种异步 / 延迟执行策略选择机制。

4、std::async 对返回值和异常处理更加方便

std::thread 本身没有提供线程函数返回值的机制,通常需要通过:

  • 共享变量;
  • std::promise;
  • 条件变量;
  • 其他线程间通信机制

来传递结果;而 std::async 天然与 std::future 配合,可以直接获取任务返回值。异常处理也类似:

  • std::thread 中线程函数抛出的未捕获异常,会导致程序调用 std::terminate();
  • std::async 会将任务中的异常保存到共享状态中,在通过 std::future 获取执行结果时重新抛出异常。

5、线程数量并不是越多越好

无论是 std::thread 还是使用 std::launch::async 的 std::async,都需要消耗系统线程资源。如果短时间内创建大量线程,可能导致:

  • 创建线程失败;
  • 系统资源消耗过大;
  • 线程上下文切换增加;
  • 程序性能下降。

因此,对于大量并发任务,通常不应该简单地通过不断创建 std::thread 或 std::async 来解决,而应该考虑线程池、任务队列等机制。

总结

  • 在 C++ 中,std::thread 是对线程进行管理,std::async 是对异步任务进行管理。
  • 如果关注的是线程本身的创建、生命周期以及线程控制,可以优先考虑使用 std::thread。
  • 如果关注的是任务的异步执行、返回结果、异常处理以及等待任务完成,使用 std::async 通常更加方便。
  • 从抽象层次来看,std::thread 偏向 线程级别 → 更关注手动管理线程,而 std::async 偏向 任务级别 → 更关注任务执行结果。

std::future

这里不再介绍 std::future 的基础使用,而是重点介绍 std::future 进阶使用的内容。

wait_for () 的使用

std::future::wait_for() 用于等待异步任务在指定时间内完成,它不会获取任务结果,而是阻塞当前线程一段时间,并通过返回值表示任务当前状态。返回值类型为 std::future_status,包含三种状态:ready 表示异步任务已完成,可以通过 get() 获取结果;timeout 表示等待时间结束,但任务仍未完成;deferred 表示任务采用延迟执行策略,需要调用 get() 或 wait() 时才会执行。wait_for() 适合用于超时检测、周期性检查异步任务状态等场景,常与 std::async、std::promise、std::packaged_task 等异步机制配合使用。

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

int process(int milliseconds) {
std::cout << "process() start, current thread id " << std::this_thread::get_id() << std::endl;
// 模拟业务处理耗时
const std::chrono::milliseconds ms(milliseconds);
std::this_thread::sleep_for(ms);
std::cout << "process() end, current thread id " << std::this_thread::get_id() << std::endl;
// 返回业务处理结果
return 5;
}

int main() {
std::cout << "main() run, thread id " << std::this_thread::get_id() << std::endl;
// 启动一个子线程,第二个参数是线程函数的参数
std::future<int> result = std::async(std::launch::async, process, 5000);
std::cout << "continue ..." << std::endl;

// 等待指定时间
std::future_status status = result.wait_for(std::chrono::seconds(2));

// 判断 Future 的状态(任务执行状态)
if (status == std::future_status::timeout) {
std::cout << "future timeout" << std::endl;
}
else if (status == std::future_status::ready) {
std::cout << "future ready" << std::endl;
}else if (status == std::future_status::deferred) {
std::cout << "future deferred" << std::endl;
} else {
std::cout << "future unknow status" << std::endl;
}

std::cout << "main() end, thread id " << std::this_thread::get_id() << std::endl;

// main 线程执行完成后,程序不会立即结束;因为 std::future 对象析构时,会等待对应的异步任务执行完成
return 0;
}

程序运行的结果如下:

1
2
3
4
5
6
main() run, thread id 1
continue ...
process() start, current thread id 2
future timeout
main() end, thread id 1
process() end, current thread id 2

特别注意

  • std::future::wait_for() 可以用于在指定时间内等待异步任务完成,并通过返回值 std::future_status 判断任务当前状态。需要注意的是,wait_for() 本身不会影响异步任务的执行,也不会取消任务,只是用于检测任务是否在指定时间内完成。
  • 当使用 std::async(std::launch::async, ...) 创建异步任务时,即使 wait_for() 返回 timeout,表示任务仍在执行,异步线程也会继续运行。若对应的 std::future 对象生命周期结束,在析构阶段会等待异步任务执行完成后再退出程序。因此,wait_for() 适用于超时检测、任务状态轮询等场景,但不能用于终止正在执行的异步任务。

std::shared_future

std::shared_future 的概述

  • 概述:

    • std::shared_future 是 C++ 11 引入的异步结果共享机制,用于允许多个线程或多个对象共享同一个异步结果。
    • 它与 std::future 类似,都可以获取异步任务的结果,但 std::future 通常只能由一个对象获取结果,而 std::shared_future 支持多个对象重复访问同一个结果。
  • 特点:

    • 异步结果共享:多个 std::shared_future 对象可以共享同一个异步结果。
    • 支持重复获取:可以多次调用 get() 获取相同的结果,而 std::future 的 get() 通常只能调用一次。
    • 线程间共享结果:适合将同一个异步结果传递给多个线程。
    • 支持异常共享:如果异步任务产生异常,多个 std::shared_future 对象调用 get() 时都可以获得该异常。
    • 支持拷贝:std::shared_future 支持拷贝,多个对象可以共享同一个共享状态。
    • 可由 std::future 转换:可以通过 std::future::share() 将 std::future 转换为 std::shared_future,转换后原 std::future 不再有效。
  • 常用成员函数:

    • get():获取异步任务的结果,可以被多次调用。
    • wait():等待异步任务完成。
    • wait_for():等待指定时间,并返回任务状态。
    • wait_until():等待到指定时间点,并返回任务状态。
    • valid():判断当前 shared_future 是否关联有效的共享状态。
  • 与 std::future 的区别:

    组件作用
    std::future独占异步结果,通常只能获取一次结果
    std::shared_future共享异步结果,可以被多个对象、多个线程获取

总结

std::shared_future 是一种可共享、可重复获取的异步结果对象,主要解决 std::future 结果只能由单个对象获取的问题。它适合需要将同一个异步结果提供给多个线程或多个消费者的场景。

std::shared_future 的使用

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

int process(const int milliseconds) {
std::cout << "process() start, current thread id " << std::this_thread::get_id() << std::endl;

// 模拟业务处理耗时
const std::chrono::milliseconds ms(milliseconds);
std::this_thread::sleep_for(ms);

std::cout << "process() end, current thread id " << std::this_thread::get_id() << std::endl;

// 返回业务处理结果
return 5;
}

void process2(const std::shared_future<int> &result) {
std::cout << "process2() start, current thread id " << std::this_thread::get_id() << std::endl;

// 获取其他线程的执行结果
std::cout << "result = " << result.get() << std::endl;

std::cout << "process2() end, current thread id " << std::this_thread::get_id() << std::endl;
}

int main() {
std::cout << "main() run, thread id " << std::this_thread::get_id() << std::endl;

// 创建 packaged_task,参数是可调用对象(比如普通函数)
std::packaged_task<int(int)> task(process);

// 获取子线程的执行结果,特别注意:get_future() 必须在 packaged_task 执行前调用
std::future<int> result = task.get_future();

// 将 future 转换为 shared_future
// std::shared_future<int> result2(std::move(result));
std::shared_future<int> result2(result.share());

// 创建子线程 1(第二个参数是线程函数的参数),子线程会直接执行
std::thread t1(std::ref(task), 5000);

// 创建子线程 2(第二个参数是线程函数的参数),子线程会直接执行
std::thread t2(process2, std::ref(result2));

// 等待子线程 1 执行完成
t1.join();

// 等待子线程 2 执行完成
t2.join();

// 获取其他线程的执行结果
const int num = result2.get();
std::cout << "result = " << num << std::endl;

std::cout << "main() end, thread id " << std::this_thread::get_id() << std::endl;

return 0;
}

程序运行的结果如下:

1
2
3
4
5
6
7
8
main() run, thread id 1
process() start, current thread id 2
process2() start, current thread id 3
process() end, current thread id 2
result = 5
process2() end, current thread id 3
result = 5
main() end, thread id 1

std::atomic

原子操作(Atomic Operation)是指在执行过程中不会被任何其他操作中断或干扰的最小、不可分割的操作单元。它要么全部执行成功,要么全部执行失败,不会出现一半执行成功一半执行失败的状态。在多线程或多进程环境中,原子操作对于保证数据一致性至关重要,因为当多个线程同时访问和修改共享数据时,原子操作能确保某一时刻只有一个线程能完成该操作,从而避免了数据竞争和不一致问题。硬件层面,CPU 提供了如比较并交换(CAS)等指令来直接支持原子操作;软件层面,编程语言和操作系统则通过锁、信号量等机制,或提供专门的原子类型和函数,来确保操作的原子性。原子操作是构建并发程序、无锁数据结构以及各种同步机制的基础基石。

std::atomic 的概述

  • 概述:

    • std::atomic 是 C++ 11 引入的原子操作机制,用于对共享数据进行线程安全的原子操作,避免多个线程同时访问共享变量时产生数据竞争。
    • 它通常用于多线程环境中的计数器、状态标志、共享变量等场景,可以保证对变量的读写操作具有原子性。
  • 特点:

    • 原子操作:对 std::atomic 对象的读写、修改等操作具有原子性,不会被其他线程看到中间状态。
    • 避免数据竞争:多个线程同时操作同一个 std::atomic 对象时,可以避免因非原子访问导致的数据竞争。
    • 线程安全:适合在多个线程之间安全地共享和修改简单类型的数据。
    • 高效轻量:相比使用互斥锁保护简单共享变量,原子操作通常具有更低的开销。
    • 支持多种操作:支持加载、存储、交换、比较并交换以及算术运算等原子操作。
    • 支持内存序:可以通过 std::memory_order 指定不同的内存序,控制多线程之间的内存访问顺序。
  • 常用成员函数:

    • load():原子地读取当前值。
    • store():原子地写入新值。
    • exchange():原子地替换当前值,并返回替换之前的值。
    • compare_exchange_weak():比较当前值与期望值,如果相等则更新为新值;失败时允许出现伪失败。
    • compare_exchange_strong():比较当前值与期望值,如果相等则更新为新值,不允许出现伪失败。
    • fetch_add():原子地执行加法操作,并返回操作之前的值。
    • fetch_sub():原子地执行减法操作,并返回操作之前的值。
    • fetch_and():原子地执行按位与操作,并返回操作之前的值。
    • fetch_or():原子地执行按位或操作,并返回操作之前的值。
    • fetch_xor():原子地执行按位异或操作,并返回操作之前的值。

std::atomic 的使用

C++ 存在线程安全的代码

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

// 定义全局变量
static int g_count = 0;

void process() {
for (int i = 0; i < 1000000; ++i) {
g_count++;
}
}

int main() {
// 创建线程 1
std::thread t1(process);

// 创建线程 2
std::thread t2(process);

// 等待线程 1 执行完成
t1.join();

// 等待线程 2 执行完成
t2.join();

// 预期输出结果是:2000000
std::cout << "count = " << g_count << std::endl;

return 0;
}

程序运行的结果如下:

1
count = 1088752

特别注意

上面这段案例代码存在线程安全问题。g_count 是多个线程共享的普通变量,而 g_count++ 并不是原子操作,实际上包含读取、加 1 和写回三个步骤。当两个线程同时执行时,可能读取到相同的旧值,导致更新丢失,因此存在数据竞争。更严格地说,在 C++ 内存模型下,数据竞争会导致未定义行为。join() 只能保证主线程等待两个子线程执行完成,并不能解决两个子线程之间对 g_count 的并发访问问题。可以使用 std::atomic<int> 对 g_count 进行原子化,或者使用 std::mutex 对临界区进行加锁,从而保证线程安全。

使用 std::atomic 解决线程安全问题

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
#include <atomic>
#include <iostream>
#include <thread>

// 定义全局变量(使用原子类型)
static std::atomic<int> g_count(0);

void process() {
for (int i = 0; i < 1000000; ++i) {
g_count++;
}
}

int main() {
// 创建子线程 1
std::thread t1(process);

// 创建子线程 2
std::thread t2(process);

// 等待子线程 1 执行完成
t1.join();

// 等待子线程 2 执行完成
t2.join();

// 预期输出结果是:2000000
std::cout << "count = " << g_count.load() << std::endl;

return 0;
}

程序运行的结果如下:

1
count = 2000000

std::atomic 的注意事项

std::atomic 原子操作主要用于多线程环境下保证共享数据访问的线程安全性。对于整数类型,通常支持自增(++)、自减(--)、加法赋值(+=)、减法赋值(-=)、按位与赋值(&=)、按位或赋值(|=)以及按位异或赋值(^=)等原子运算,但具体支持的操作取决于原子类型和编译器实现,并非所有操作都适用于所有 std::atomic 类型。

std::atomic 不支持原子操作的写法(存在线程安全问题)

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
#include <iostream>

#include <atomic>
#include <thread>

// 定义全局变量(使用原子类型)
static std::atomic<int> g_count(0);

void process() {
for (int i = 0; i < 1000000; ++i) {
// 下面这种写法不支持原子操作,存在线程安全问题
g_count = g_count + 1;
}
}

int main() {
// 创建子线程 1
std::thread t1(process);

// 创建子线程 2
std::thread t2(process);

// 等待子线程 1 执行完成
t1.join();

// 等待子线程 2 执行完成
t2.join();

// 预期输出结果是:2000000
std::cout << "count = " << g_count.load() << std::endl;

return 0;
}

程序运行的结果如下:

1
count = 1264039

其他常见的互斥量

std::recursive_mutex

std::recursive_mutex 的概述
  • 定义:std::recursive_mutex 是 C++ 11 提供的递归互斥锁,也称为可重入锁。
  • 特点:同一个线程可以多次获取同一把锁,不会因为重复加锁而导致死锁。
  • 解锁规则:每次 lock() 都必须对应一次 unlock(),只有加锁次数归零后,锁才真正释放。
  • 适用场景:适用于类的多个成员函数相互调用或者递归调用,并且这些函数都需要对同一资源加锁的场景。
  • 注意:如果没有递归加锁的需求,推荐优先使用 std::mutex,因为它的语义更加简单明确。

总结

std::recursive_mutex 是支持同一线程多次获取同一把锁的互斥量,每次加锁(lock())都必须对应一次解锁(unlock())。

std::recursive_mutex 的使用

在下面的案例代码中,使用了 std::recursive_mutex,因此同一个线程可以重复获取 m_mutex,不会因为递归加锁而导致线程死锁。特别注意,如果将它换成 std::mutex m_mutex;,递归调用 process() 时,同一个线程再次加锁,就会产生线程死锁。

使用 std::recursive_mutex + std::unique_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
#include <iostream>
#include <mutex>

class MyClass {
public:
void process(int count) {
// 获取锁
std::unique_lock<std::recursive_mutex> lock(m_mutex);

std::cout << "count = " << count << std::endl;

if (count > 0) {
// 递归调用,再次获取同一把锁
process(count - 1);
}

// 这里不需要手动调用 unlock(),离开作用域后,unique_lock 析构时会自动释放锁
}

private:
std::recursive_mutex m_mutex; // 递归互斥锁
};

int main() {
MyClass obj;

obj.process(3);

return 0;
}

程序运行的结果如下:

1
2
3
4
count = 3
count = 2
count = 1
count = 0

std::timed_mutex

std::timed_mutex 的概述
  • 定义:std::timed_mutex 是 C++ 11 提供的带超时功能的互斥锁。
  • 特点:除了支持普通的加锁、解锁外,还支持尝试加锁并设置等待时间。
  • 超时机制:如果在指定时间内获取不到锁,可以放弃等待并继续执行其他逻辑,避免线程无限阻塞。
  • 常用成员函数:
    • try_lock_for():等待指定时间。
    • try_lock_until():等待到指定时间点。
  • 适用场景:适用于不希望线程长时间阻塞,需要设置加锁超时时间的场景。
  • 注意:如果不需要超时控制,通常使用 std::mutex 即可。
std::timed_mutex 的使用

案例一:std::timed_mutex 使用

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

class MyClass {

public:
void process() {
// 尝试获取锁,最多等待 3 秒
if (m_mutex.try_lock_for(std::chrono::seconds(3))) {
std::cout << "成功获取锁, 线程 ID 是 " << std::this_thread::get_id() << std::endl;

// 模拟业务处理耗时
std::this_thread::sleep_for(std::chrono::seconds(5));

// 解锁
m_mutex.unlock();
}
else {
std::cout << "获取锁超时, 线程 ID 是 " << std::this_thread::get_id() << std::endl;
}
}
private:
std::timed_mutex m_mutex; // 带超时功能的互斥锁
};

int main() {
MyClass obj;

// 创建并启动线程 1
std::thread t1(&MyClass::process, &obj);

// 创建并启动线程 2
std::thread t2(&MyClass::process, &obj);

// 等待线程 1 执行完成
t1.join();

// 等待线程 2 执行完成
t2.join();

return 0;
}

程序运行的结果如下:

1
2
成功获取锁, 线程 ID 是 2
获取锁超时, 线程 ID 是 3

案例二:std::timed_mutex + std::unique_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
#include <chrono>
#include <iostream>
#include <mutex>
#include <thread>

class MyClass {
public:
void process() {
// 先创建 unique_lock,但暂时不加锁
std::unique_lock<std::timed_mutex> lock(m_mutex, std::defer_lock);

// 尝试获取锁,最多等待 3 秒
if (lock.try_lock_for(std::chrono::seconds(3))) {
std::cout << "成功获取锁, 线程 ID 是 " << std::this_thread::get_id() << std::endl;

// 模拟业务处理耗时
std::this_thread::sleep_for(std::chrono::seconds(5));

// 这里不需要手动调用 unlock(),离开作用域后,unique_lock 析构时会自动释放锁
} else {
std::cout << "获取锁超时, 线程 ID 是 " << std::this_thread::get_id() << std::endl;
}
}

private:
std::timed_mutex m_mutex; // 带超时功能的互斥锁
};

int main() {
MyClass obj;

// 创建并启动线程 1
std::thread t1(&MyClass::process, &obj);

// 创建并启动线程 2
std::thread t2(&MyClass::process, &obj);

// 等待线程 1 执行完成
t1.join();

// 等待线程 2 执行完成
t2.join();

return 0;
}

程序运行的结果如下:

1
2
成功获取锁, 线程 ID 是 2
获取锁超时, 线程 ID 是 3

std::recursive_timed_mutex

std::recursive_timed_mutex 的概述
  • 定义:std::recursive_timed_mutex 是 C++ 11 提供的支持递归加锁和超时机制的互斥锁。
  • 递归加锁:同一个线程可以多次获取同一把锁,不会因为重复加锁而死锁。
  • 超时机制:支持在指定时间内尝试获取锁,超时后返回失败。
  • 常用成员函数:
    • try_lock_for():尝试获取锁,最多等待指定时间。
    • try_lock_until():尝试获取锁,等待到指定时间点。
  • 解锁规则:每次 lock() 都必须对应一次 unlock(),只有加锁次数归零后,锁才真正释放。
  • 适用场景:既需要递归加锁,又需要超时控制的场景。

总结

std::recursive_timed_mutex = std::recursive_mutex 的递归能力 + std::timed_mutex 的超时能力。

std::recursive_timed_mutex 的使用

使用 std::recursive_timed_mutex + std::unique_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
#include <chrono>
#include <iostream>
#include <mutex>
#include <thread>

class MyClass {
public:
void process(int count) {
// 先创建 unique_lock,但暂时不加锁
std::unique_lock<std::recursive_timed_mutex> lock(m_mutex, std::defer_lock);

if (lock.try_lock_for(std::chrono::seconds(3))) {
std::cout << "成功获取锁, 线程 ID 是 " << std::this_thread::get_id() << ", count = " << count << std::endl;

// 模拟业务处理耗时
std::this_thread::sleep_for(std::chrono::seconds(2));

if (count > 0) {
// 递归调用,再次获取同一把锁
process((count - 1));
}

// 这里不需要手动调用 unlock(),离开作用域后,unique_lock 析构时会自动释放锁
} else {
std::cout << "获取锁超时, 线程 ID 是 " << std::this_thread::get_id() << std::endl;
}
}

private:
std::recursive_timed_mutex m_mutex; // 支持递归加锁和超时功能的互斥锁
};

int main() {
MyClass obj;

obj.process(3);

return 0;
}

程序运行的结果如下:

1
2
3
4
成功获取锁, 线程 ID 是 1, count = 3
成功获取锁, 线程 ID 是 1, count = 2
成功获取锁, 线程 ID 是 1, count = 1
成功获取锁, 线程 ID 是 1, count = 0