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::futureget()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::futureget()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::futurewait_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::threadstd::async 都可以用于 C++ 的异步执行,但两者的设计目标不同:std::thread 更偏向于直接管理线程,而 std::async 更偏向于管理异步任务及其执行结果。

对比项std::threadstd::async
核心定位线程管理异步任务管理
返回值不直接提供返回值可以通过 std::future 获取返回值
异常处理线程内部异常不能直接传递给创建线程异常会保存到 std::future,获取结果时可以重新抛出异常
线程创建通常直接创建一个新的线程可以选择异步执行,也可以延迟执行
启动策略创建线程后立即开始执行支持 std::launch::asyncstd::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::asyncstd::async,都需要消耗系统线程资源。如果短时间内创建大量线程,可能导致:

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

因此,对于大量并发任务,通常不应该简单地通过不断创建 std::threadstd::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::asyncstd::promisestd::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::futureget() 通常只能调用一次。
    • 线程间共享结果:适合将同一个异步结果传递给多个线程。
    • 支持异常共享:如果异步任务产生异常,多个 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

Windows 临界区

Windows 临界区的概述

Windows 临界区(CRITICAL_SECTION)是 Windows 系统提供的一种线程同步机制,主要用于保护进程内多个线程访问共享数据,避免多个线程同时访问共享资源而产生数据竞争。

  • 核心特点

    • 进程内使用:只能用于同一个进程中的线程之间进行同步。
    • 互斥访问:同一时刻通常只有一个线程能够进入临界区,其他线程需要等待。
    • 轻量级:相比 Windows 内核互斥对象,临界区通常具有更低的开销,适合频繁进行线程同步的场景。
    • 需要初始化和销毁:使用前需要初始化,使用结束后需要释放相关资源。
    • 手动加锁和解锁:进入临界区后必须确保最终能够离开,否则可能导致其他线程长期等待甚至死锁。
    • 不支持跨进程同步:如果需要实现不同进程之间的同步,应使用 Windows 的互斥体(Mutex)等内核同步对象。
  • std::mutex 的关系

    • 从功能上看,Windows 系统的 CRITICAL_SECTION 与 C++ 标准库的 std::mutex 都可以用于实现线程之间的互斥访问。
    • 主要区别在于:CRITICAL_SECTION 是 Windows 平台相关的 API,而 std::mutex 是 C++ 标准库提供的跨平台同步工具。
    • 因此,在 Windows 专用程序中可以使用 CRITICAL_SECTION;如果希望代码具有更好的跨平台能力,通常更推荐使用 std::mutex
    • 注意:
      • CRITICAL_SECTION 支持同一个线程多次进入同一个 Windows 临界区,属于递归锁(可重入锁)。
      • 但是,std::mutex 不支持递归加锁,同一线程多次调用 lock() 会导致阻塞,因此不具备可重入性
      • 在 C++ 11 中,如果需要支持同一线程重复加锁,应该使用 std::recursive_mutex
  • 多次进入同一个临界区

    • 同一个线程可以多次进入同一个 Windows 临界区,这里类似于 Java 的可重入锁(synchronizedReentrantLock 等)
    • Windows 临界区支持递归进入:同一个线程在已经进入临界区的情况下,可以再次调用进入临界区操作,不会发生阻塞
    • 内部通过 “递归计数” 管理:Windows 临界区会记录同一线程进入临界区的次数。
    • 进入与离开需要匹配:调用多少次进入临界区操作,就需要调用相同次数的离开临界区操作,直到计数归零后,临界区才真正释放。
    • 注意:如果进入次数与离开次数不匹配,可能导致其他线程长期无法进入临界区,造成线程死锁问题。
    • 举例:
      1
      2
      3
      4
      5
      EnterCriticalSection(&winsec);  // 第一次进入临界区(加锁)
      EnterCriticalSection(&winsec); // 第二次进入临界区(加锁)
      msgRecvQueue.push_back(i);
      LeaveCriticalSection(&winsec); // 第一次离开临界区(解锁)
      LeaveCriticalSection(&winsec); // 第二次离开临界区(解锁)

总结

Windows 临界区是一种轻量级的进程内线程同步机制,用于保证同一时刻只有一个线程可以访问受保护的共享资源。它性能较好,但属于 Windows 系统专用 API,现代 C++ 开发中通常推荐优先考虑使用 std::mutex(互斥量)。特别注意,Windows 的 CRITICAL_SECTIONstd::mutex 的一个重要区别:Windows 临界区允许同一线程递归进入(即可重入),而 std::mutex 不允许

Windows 临界区的使用

使用案例一

案例背景说明

在网络游戏服务器的设计中,共享数据的保护是一个典型案例:可以创建两个线程,其中一个线程负责收集玩家的命令并将命令数据写入队列,另一个线程则从队列中取出玩家发来的命令,进行解析并执行玩家所需的动作。值得一提的是,在当前业务场景下建议使用生产者消费者模型来实现,并使用 list 容器作为队列。

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
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
#include <atomic>
#include <chrono>
#include <iostream>
#include <list>
#include <mutex>
#include <thread>
#include <Windows.h>

#define WINDOWS_CRITICAL_SECTION // 标记 Windows 系统环境

class MyClass {
public:
// 将收到的玩家命令写入队列
void inMsgRecvQueue() {
for (int i = 0; i < 1000; ++i) {
#ifdef WINDOWS_CRITICAL_SECTION
// 加锁
EnterCriticalSection(&winsec);

// 插入队列
msgRecvQueue.push_back(i);

// 解锁
LeaveCriticalSection(&winsec);
#else
{
// 加锁(出了作用域后会自动解锁)
std::unique_lock<std::mutex> lock(msgRecvQueueMutex);

// 插入队列
msgRecvQueue.push_back(i);
}
#endif

// 模拟网络收包间隔
std::this_thread::sleep_for(std::chrono::milliseconds(1));
}

// 更新程序停止标记
stop = true;
}

// 从队列中读取玩家命令
void outMsgRecvQueue() {
while (true) {
int command = -1;

#ifdef WINDOWS_CRITICAL_SECTION
// 加锁
EnterCriticalSection(&winsec);

// 判断程序停止标记
if (stop && msgRecvQueue.empty()) {
// 解锁
LeaveCriticalSection(&winsec);
break;
}

// 操作队列
if (!msgRecvQueue.empty()) {
// 取出队列元素
command = msgRecvQueue.front();
// 移除队列元素
msgRecvQueue.pop_front();
}

// 解锁
LeaveCriticalSection(&winsec);
#else
{
// 加锁(出了作用域后会自动解锁)
std::unique_lock<std::mutex> lock(msgRecvQueueMutex);

// 判断程序停止标记
if (stop && msgRecvQueue.empty()) {
break;
}

// 操作队列
if (!msgRecvQueue.empty()) {
// 取出队列元素
command = msgRecvQueue.front();
// 移除队列元素
msgRecvQueue.pop_front();
}
}
#endif

// 打印
if (command != -1) {
std::cout << "已处理玩家命令: " << command << std::endl;
}

// 模拟业务执行耗时
std::this_thread::sleep_for(std::chrono::milliseconds(1));
}
}

// 构造函数
MyClass() {
#ifdef WINDOWS_CRITICAL_SECTION
InitializeCriticalSection(&winsec); // 初始化临界区
#endif
}

// 析构函数
~MyClass() {
#ifdef WINDOWS_CRITICAL_SECTION
DeleteCriticalSection(&winsec); // 删除临界区
#endif
}

private:
std::list<int> msgRecvQueue; // 消息队列(共享数据)
std::mutex msgRecvQueueMutex; // 保护消息队列线程安全的互斥锁
std::atomic_bool stop{false}; // 程序停止标记

#ifdef WINDOWS_CRITICAL_SECTION
CRITICAL_SECTION winsec; // Windows 系统中的临界区,作用非常类似于 C++ 11 中的 std::mutex
#endif

};

int main() {
std::cout << "main thread start." << std::endl;

// 局部变量
MyClass mc;

// 创建并启动写线程
std::thread t_write(&MyClass::inMsgRecvQueue, &mc);

// 创建并启动读线程
std::thread t_read(&MyClass::outMsgRecvQueue, &mc);

// 等待写线程执行完毕
t_write.join();

// 等待读线程执行完毕
t_read.join();

std::cout << "main thread end." << std::endl;
return 0;
}

程序运行的结果如下:

1
2
3
4
5
6
7
8
9
已处理玩家命令: 0
已处理玩家命令: 1
已处理玩家命令: 2
已处理玩家命令: 3
......
已处理玩家命令: 996
已处理玩家命令: 997
已处理玩家命令: 998
已处理玩家命令: 999

特别注意

Windows 系统的 CRITICAL_SECTION 本身不像 std::lock_guard 那样可以自动管理锁,更类似于 std::mutex,所以使用时加锁后千万不要忘记解锁

使用案例二

Windows 系统的 CRITICAL_SECTION 不支持自动离开临界区(自动解锁),为了实现 std::lock_guard 自动解锁的功能,可以使用以下方式封装一个自定义的 RAII 类(比如 CWinLock)。值得注意的是,CWinLock 支持递归锁(可重入锁)。CWinLock 基于 Windows CRITICAL_SECTION 实现,而 CRITICAL_SECTION 支持同一线程递归进入(可重入)。因此,同一线程可以递归获取同一个 CWinLock,每次进入都会对应一次离开;只有进入次数降为 0 后,临界区才真正被释放;而不同线程尝试进入同一个临界区时,仍然需要等待当前持锁线程完全退出(释放锁)。

递归锁(可重入锁)的概念

递归锁(可重入锁)是指:同一个线程可以多次获取同一把锁,而不会因为重复加锁导致死锁;但每次加锁都必须对应一次解锁,直到加锁次数归零后,锁才真正释放。

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
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
#include <atomic>
#include <chrono>
#include <iostream>
#include <list>
#include <mutex>
#include <thread>
#include <windows.h>

#define WINDOWS_CRITICAL_SECTION // 标记 Windows 系统环境

#ifdef WINDOWS_CRITICAL_SECTION
// RAII 类,用于自动释放 Windows 的临界区,防止忘记释放临界区导致线程死锁问题的发生,类似于 C++ 11 中的 std::lock_guard
class CWinLock {
public:
// 构造函数
CWinLock(CRITICAL_SECTION * pCritical) : m_pCritical(pCritical) {
EnterCriticalSection(pCritical); // 进入临界区
}

// 析构函数
~CWinLock() {
LeaveCriticalSection(m_pCritical); // 释放临界区
}

// 禁止拷贝构造
CWinLock(const CWinLock &lock) = delete;

// 禁止拷贝赋值
CWinLock& operator=(const CWinLock &lock) = delete;

private:
CRITICAL_SECTION* const m_pCritical; // Windows 临界区
};
#endif

class MyClass {
public:
// 将收到的玩家命令写入队列
void inMsgRecvQueue() {
for (int i = 0; i < 1000; ++i) {
#ifdef WINDOWS_CRITICAL_SECTION
{
// 加锁(出了作用域会自动解锁)
CWinLock lock(&winsec);

// 插入队列
msgRecvQueue.push_back(i);
}
#else
{
// 加锁(出了作用域后会自动解锁)
std::unique_lock<std::mutex> lock(msgRecvQueueMutex);

// 插入队列
msgRecvQueue.push_back(i);
}
#endif

// 模拟网络收包间隔
std::this_thread::sleep_for(std::chrono::milliseconds(1));
}

// 更新程序停止标记
stop = true;
}

// 从队列中读取玩家命令
void outMsgRecvQueue() {
while (true) {
int command = -1;

#ifdef WINDOWS_CRITICAL_SECTION
{
// 加锁(出了作用域会自动解锁)
CWinLock lock(&winsec);

// 判断程序停止标记
if (stop && msgRecvQueue.empty()) {
break;
}

// 操作队列
if (!msgRecvQueue.empty()) {
// 取出队列元素
command = msgRecvQueue.front();
// 移除队列元素
msgRecvQueue.pop_front();
}
}
#else
{
// 加锁(出了作用域后会自动解锁)
std::unique_lock<std::mutex> lock(msgRecvQueueMutex);

// 判断程序停止标记
if (stop && msgRecvQueue.empty()) {
break;
}

// 操作队列
if (!msgRecvQueue.empty()) {
// 取出队列元素
command = msgRecvQueue.front();
// 移除队列元素
msgRecvQueue.pop_front();
}
}
#endif

// 打印
if (command != -1) {
std::cout << "已处理玩家命令: " << command << std::endl;
}

// 模拟业务执行耗时
std::this_thread::sleep_for(std::chrono::milliseconds(1));
}
}

// 构造函数
MyClass() {
#ifdef WINDOWS_CRITICAL_SECTION
InitializeCriticalSection(&winsec); // 初始化临界区
#endif
}

// 析构函数
~MyClass() {
#ifdef WINDOWS_CRITICAL_SECTION
DeleteCriticalSection(&winsec); // 删除临界区
#endif
}

private:
std::list<int> msgRecvQueue; // 消息队列(共享数据)
std::mutex msgRecvQueueMutex; // 保护消息队列线程安全的互斥锁
std::atomic_bool stop{false}; // 程序停止标记

#ifdef WINDOWS_CRITICAL_SECTION
CRITICAL_SECTION winsec; // Windows 系统中的临界区,作用非常类似于 C++ 11 中的 std::mutex
#endif

};

int main() {
std::cout << "main thread start." << std::endl;

// 局部变量
MyClass mc;

// 创建并启动写线程
std::thread t_write(&MyClass::inMsgRecvQueue, &mc);

// 创建并启动读线程
std::thread t_read(&MyClass::outMsgRecvQueue, &mc);

// 等待写线程执行完毕
t_write.join();

// 等待读线程执行完毕
t_read.join();

std::cout << "main thread end." << std::endl;
return 0;
}

程序运行的结果如下:

1
2
3
4
5
6
7
8
9
已处理玩家命令: 0
已处理玩家命令: 1
已处理玩家命令: 2
已处理玩家命令: 3
......
已处理玩家命令: 996
已处理玩家命令: 997
已处理玩家命令: 998
已处理玩家命令: 999

总结

通过 RAII 封装 CRITICAL_SECTION,在构造时自动进入临界区,在析构时自动释放临界区,从而避免因遗漏 LeaveCriticalSection() 而导致线程死锁。CRITICAL_SECTION 负责临界区本身的生命周期,CWinLock 负责一次加锁 / 解锁操作的 RAII 管理。值得注意的是,不建议在 CWinLock 这个 RAII 类中调用 InitializeCriticalSection() / DeleteCriticalSection(),因为 CWinLock 的职责只是管理一次 Enter/Leave,而不是管理 CRITICAL_SECTION 本身的生命周期。

其他互斥量使用

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