Mutex Classes
Mutex 代表互斥。使用互斥体的基本机制如下:
- 想要使用与其他线程共享(读/写)内存的线程尝试锁定互斥对象。如果另一个线程当前持有此锁,则想要获得访问权限的新线程会阻塞,直到锁被释放或超时间隔到期。
- 一旦线程获得了锁,就可以自由使用共享内存。当然,这假设所有想要正确使用共享数据的线程都获取互斥锁上的锁。
- 当线程完成对共享内存的读/写操作后,它会释放锁,以便其他线程有机会获得对共享内存的锁。如果两个或多个线程正在等待锁,则无法保证哪个线程将被授予锁并因此允许继续进行。
C++ 标准库以递归和非递归的方式提供非定时和定时互斥类。在讨论所有这些选项之前,让我们首先看一下称为自旋锁的概念。
自旋锁
自旋锁是 mutex 的一种形式,其中线程使用繁忙循环(自旋)来尝试获取锁、执行其工作并释放锁。自旋时,线程保持活动状态,但不做任何有用的工作。即便如此,自旋锁在某些情况下还是有用的,因为它们可以完全用你自己的代码实现,并且不需要对操作系统进行任何昂贵的调用,不会产生任何线程切换的开销。如以下代码片段所示,可以使用单个原子类型:atomic_flag
来实现自旋锁。
atomic_flag spinlock = ATOMIC_FLAG_INIT; // Uniform initialization is not allowed.
static const size_t NumberOfThreads { 50 };
static const size_t LoopsPerThread { 100 };
void dowork(size_t threadNumber, vector<size_t>& data) {
for (size_t i { 0 }; i < LoopsPerThread; ++i) {
while (spinlock.test_and_set()) { } // Spins until lock is acquired.
// Save to handle shared data...
data.push_back(threadNumber);
spinlock.clear(); // Releases the acquired lock.
}
}
int main() {
vector<size_t> data;
vector<thread> threads;
for (size_t i { 0 }; i < NumberOfThreads; ++i) {
threads.push_back(thread { dowork, i, ref(data) });
}
for (auto& t : threads) {
t.join();
}
cout << format("data contains {} elements, expected {}.\n", data.size(), NumberOfThreads * LoopsPerThread);
}
在此代码中,每个线程尝试通过在atomic_flag上重复调用test_and_set()来获取锁,直到成功。这是繁忙循环。
由于自旋锁使用繁忙的等待循环,因此只有当你确定线程只会在短时间内锁定自旋锁时,才应该选择它们。
lock_guard
RAII思想,将锁视为资源
#include <mutex>
#include <thread>
int main() {
std::vector<int> arr;
std::mutex mtx;
std::thread t1([&] {
for (int i = 0; i < 1000; i++) {
std::lock_guard<std::mutex> grd(mtx);
arr.push_back(1);
}
});
std::thread t2([&] {
for (int i = 0; i < 1000; i++) {
std::lock_guard<std::mutex> grd(mtx);
arr.push_back(2);
}
});
t1.join();
t2.join();
}
严格控制在作用域释放时解锁,想要提前解锁可以用std::unique_lock
的unlock
try_lock
上锁失败不阻塞
#include <mutex>
#include <thread>
int main() {
std::mutex mtx;
if (mtx.try_lock())
std::cout << "Success" << std::endl;
else
std::cout << "Failure" << std::endl;
}
try_lock_for
上锁失败,等一段时间
#include <mutex>
#include <thread>
int main() {
std::timed_mutex mtx;
if (mtx.try_lock_for(std::chrono::milliseconds(500)))
std::cout << "Success" << std::endl;
else
std::cout << "Failure" << std::endl;
}
try_lock_until
等待到时间点
死锁避免
- 只同时持有一个锁
- 按照相同顺序上锁
- 使用
std::lock
:std::lock(mtx1, mtx2, ...)
,每个锁需要单独解锁 std::lock
的RAII版本:std::scoped_lock
- 可重入锁:
std::recursive_mutex
用mutable修饰锁
可以允许逻辑上为const而过程中涉及锁的函数定义为const:
class ThreadsafeCounter {
mutable std::mutex m;
int data = 0;
public:
int get() const {
std::lock_guard<std::mutex> lk(m);
return data;
}
};
读写锁
std::shared_mutex
Locks
unique_lock
std::unique_lock
在 <mutex> 中定义,是一种更复杂的锁,它允许将锁获取推迟到执行的后期,即声明之后很久。可以使用 owns_lock() 方法或 unique_lock 的 bool 转换运算符来查看是否已获取锁。本章后面的“使用定时锁”部分给出了使用此转换运算符的示例。 unique_lock 有几个构造函数:
explicit unique_lock(mutex_type& m);
此构造函数接受对互斥锁的引用。它尝试获取互斥锁并阻塞,直到获取锁为止。unique_lock(mutex_type& m, defer_lock_t) noexcept;
此构造函数接受对互斥锁和 std::defer_lock_t 实例的引用。标准库提供了一个预定义的 defer_lock_t 实例,名为std::defer_lock
。 unique_lock 存储对互斥锁的引用,但不会立即尝试获取锁。稍后可以获得锁。unique_lock(mutex_type& m, try_to_lock_t);
此构造函数接受对互斥锁和 std::try_to_lock_t 实例的引用。标准库提供了一个预定义的 try_to_lock_t 实例,名为std::try_to_lock
。该锁尝试获取所引用互斥锁的锁,但如果失败,它不会阻塞,在这种情况下,可以稍后获取锁。unique_lock(mutex_type& m, adopt_lock_t);
此构造函数接受对互斥锁和 std::adopt_lock_t 实例的引用,例如std::adopt_lock
。该锁假定调用线程已经获得了所引用互斥体的锁。锁“采用”互斥锁,并在锁被销毁时自动释放互斥锁。unique_lock(mutex_type& m, const chrono::time_point<Clock, Duration>& abs_time);
此构造函数接受对互斥锁和绝对时间的引用。构造函数尝试获取锁,直到系统时间超过给定的绝对时间。
一次获取多个锁
scoped_lock
类似于lock_guard
,它接受可变数量的mutex。例如:
mutex mut1;
mutex mut2;
void process() {
scoped_lock locks { mut1, mut2 };
// Locks acquired.
} // Locks automatically released.
条件变量
条件变量允许一个线程阻塞,直到另一个线程设置某个条件或直到系统时间达到指定时间。这些变量允许显式的线程间通信。如果您熟悉使用 Win32 API 进行多线程编程,则可以将条件变量与 Windows 中的事件对象进行比较。
有两种条件变量可用,它们都在 <condition_variable> 中定义:
- std::condition_variable:只能在 unique_lock<mutex> 上等待的条件变量,根据 C++ 标准,它可以在某些平台上实现最大效率。
- std::condition_variable_any:可以等待任何类型对象的条件变量,包括自定义锁类型。
Latches
latch 是一次性的线程协调点。许多线程在锁存点处阻塞。一旦给定数量的线程到达锁存点,所有线程都将被解除阻塞并允许继续执行。基本上,它是一个计数器,随着每个线程到达锁存点而进行倒计时。一旦计数器达到零,锁存器将无限期地保持在有信号状态,所有阻塞线程都将被解除阻塞,并且随后到达锁存点的任何线程都将立即被允许继续。
latch 由 std::latch
实现,在 <latch> 中定义。构造函数接受需要到达锁存点的所需线程数。到达锁存点的线程可以调用arrive_and_wait(),它会递减锁存计数器并阻塞,直到锁存器收到信号为止。线程还可以通过调用 wait() 在锁存点上阻塞,而不减少计数器。 try_wait() 方法可用于检查计数器是否已达到零。最后,如果需要,还可以通过调用 count_down() 在不阻塞的情况下递减计数器。
下面演示了锁存点的一个用例,其中一些数据需要加载到内存(I/O 限制)中,随后在多个线程中并行处理。进一步假设线程在启动时以及开始处理数据之前需要执行一些 CPU 密集型初始化。通过首先启动线程并让它们执行 CPU 限制的初始化,并并行加载数据(I/O 限制),可以提高性能。该代码使用计数器 1 初始化锁存器对象,并启动 10 个线程,这些线程都执行一些初始化操作,然后阻塞锁存器,直到锁存器计数器达到零。启动 10 个线程后,代码会加载一些数据,例如从磁盘加载数据,即 I/O 密集步骤。加载所有数据后,锁存计数器将递减至 0,从而解除所有 10 个等待线程的阻塞。
latch startLatch { 1 };
vector<jthread> threads;
for (int i { 0 }; i < 10; ++i) {
threads.push_back(jthread { [&startLatch] {
// Do some initialization... (CPU bound)
// Wait until the latch counter reaches zero.
startLatch.wait();
// Process data...
} });
}
// Load data... (I/O bound)
// Once all data has been loaded, decrement the latch counter
// which then reaches zero and unblocks all waiting threads.
startLatch.count_down();
屏障
屏障是一种可重用的线程协调机制,由一系列阶段组成。许多线程在屏障点处阻塞。当给定数量的线程到达屏障时,将执行阶段完成回调,解除所有阻塞线程的阻塞,重置线程计数器,然后开始下一阶段。在每个阶段中,可以调整下一阶段的预期线程数量。屏障非常适合在循环之间执行同步。例如,假设有多个并发运行的线程并在循环中执行一些计算。进一步假设,一旦这些计算完成,需要对结果执行某些操作,然后线程才能开始其循环的新迭代。对于这种情况,屏障是完美的。所有线程都在屏障处阻塞。当它们全部到达时,阶段完成回调会处理线程的结果,然后解除所有线程的阻塞以开始下一次迭代。
屏障由std::barrier
实现,在 <barrier> 中定义。屏障最重要的方法是arrive_and_wait()
,它递减计数器,然后阻塞线程,直到当前阶段完成。有关其他可用方法的完整说明,请参阅标准库参考。
以下代码片段演示了屏障的使用。它启动四个线程,在循环中连续执行某些操作。在每次迭代中,所有线程都使用屏障进行同步。
void completionFunction() noexcept { /* ... */ }
int main() {
const size_t numberOfThreads { 4 };
barrier barrierPoint { numberOfThreads, completionFunction };
vector<jthread> threads;
for (int i { 0 }; i < numberOfThreads; ++i) {
threads.push_back(jthread { [&barrierPoint] (stop_token token) {
while (!token.stop_requested()) {
// ... Do some calculations ...
// Synchronize with other threads.
barrierPoint.arrive_and_wait();
}
} });
}
}
信号量
信号量是轻量级同步原语,可用作其他同步机制(例如互斥锁、锁存器和屏障)的构建块。基本上,信号量由代表多个槽的计数器组成。计数器在构造函数中初始化。如果获得一个槽,那么计数器递减,而释放槽则计数器递增。 <semaphore> 中定义了两个信号量类:std::counting_semaphore
和binary_semaphore
。前者对非负资源计数进行建模。后者只有一个插槽;因此,槽要么是空闲的,要么是非空闲的,非常适合作为互斥体的构建块。
FUTURES
正如本章前面所讨论的,使用std::thread
启动计算单个结果的线程并不容易在线程完成执行后获取计算结果。 std::thread 的另一个问题是它如何处理异常等错误。如果线程抛出异常并且该异常没有被线程本身捕获,则 C++ 运行时会调用 std::terminate(),这通常会终止整个应用程序。
future 可用于更轻松地从线程中获取结果,并将异常从一个线程传输到另一个线程,然后该线程可以根据需要处理异常。当然,总是尽可能地尝试在实际线程中处理异常,以防止它们离开线程仍然是一个很好的做法。
promise 是线程存储其结果的地方。 future 用于访问 promise 中存储的结果。也就是说,promise 是结果的输入端,future 是输出端。一旦在同一个线程或另一个线程中运行的函数计算出它想要返回的值,它就可以将该值放入 promise 中。然后可以通过 future 检索该值。可以将此机制视为结果的线程间通信通道。
C++ 提供了一个标准的 future,称为std::future
。你可以按如下方式从 std::future 检索结果。 T是计算结果的类型。
future<T> myFuture { ... }; // Is discussed later.
T result { myFuture.get() };
对 get() 的调用检索结果并将其存储在变量 result 中。如果计算结果尚未完成,则对 get() 的调用将阻塞,直到该值可用为止。你只能在 future 中调用一次 get() 。标准未定义第二次调用它的行为。
如果想避免阻塞,可以先询问 future 是否有结果:
if (myFuture.wait_for(0)) { // Value is available.
T result { myFuture.get() };
} else { // Value is not yet available.
...
}
std::promise 与 std::future
C++ 提供了 std::promise 类作为实现 promise 概念的一种方法。可以在 promise 上调用 set_value() 来存储结果,也可以在 promise 上调用 set_exception() 来存储 promise 中的异常。请注意,你只能对特定的 promise 调用一次 set_value() 或 set_exception() 。如果多次调用它,将会抛出 std::future_error 异常。
启动另一个线程 B 来计算某些内容的线程 A 可以创建一个 std::promise 并将其传递给启动的线程。请注意,promise 不能被复制,但可以将其移动到线程中!线程 B 使用该 promise 来存储结果。在将 promise 移入线程 B 之前,线程 A 对创建的 promise 调用 get_future(),以便在 B 完成后能够访问结果。这是一个简单的例子:
void doWork(promise<int> thePromise) {
// ... Do some work ...
// And ultimately store the result in the promise.
thePromise.set_value(42);
}
int main() {
// Create a promise to pass to the thread.
promise<int> myPromise;
// Get the future of the promise.
auto theFuture { myPromise.get_future() };
// Create a thread and move the promise into it.
thread theThread { doWork, move(myPromise) };
// Do some more work...
// Get the result.
int result { theFuture.get() };
cout << "Result: " << result << endl;
// Make sure to join the thread.
theThread.join();
}
std::packaged_task
std::packaged_task
使得使用 promise 比显式使用 std::promise 更容易,如上一节所示。下面的代码演示了这一点。它创建一个packaged_task来执行calculateSum()。通过调用 get_future() 从 packaged_task 中检索 future。启动一个线程,并将 packaged_task 移入其中。 packaged_task 无法复制。线程启动后,对检索到的 future 调用 get() 来获取结果。这会阻塞直到结果可用。
请注意,calculateSum() 不需要在任何类型的 promise 中显式存储任何内容。 packaged_task 会自动创建一个 promise,自动将被调用函数(本例中为calculateSum())的结果存储在 promise 中,并自动将函数抛出的任何异常存储在 promise 中。
int calculateSum(int a, int b) { return a + b; }
int main() {
// Create a packaged task to run calculateSum.
packaged_task<int(int, int)> task { calculateSum };
// Get the future for the result of the packaged task. auto theFuture { task.get_future() };
// Create a thread, move the packaged task into it, and
// execute the packaged task with the given arguments.
thread theThread { move(task), 39, 3 };
// Do some more work...
// Get the result.
int result { theFuture.get() };
cout << result << endl;
// Make sure to join the thread.
theThread.join();
}
std::async
如果你想让 C++ 运行时更好地控制是否创建线程来计算某些内容,可以使用 std::async()
。它接受要执行的函数并返回可用于检索结果的 future。 async() 可以通过两种方式运行函数:
- 通过在单独的线程上异步运行函数
- 当调用返回的 future 对象的 get() 时,会在当前调用线程同步执行函数
如果你在不添加其他参数的情况下调用 async(),则运行时会根据系统中的 CPU 核心数量和已发生的并发量等因素自动选择这两种方法之一。可以通过指定策略参数来影响运行时的行为:
- launch::async:强制运行时在不同线程上异步执行函数
- launch::deferred:当调用 get() 时,强制运行时在调用线程上同步执行函数
- launch::async | launch::deferred:让运行时选择(默认行为)
以下示例演示了 async() 的使用:
int calculate() { return 123; }
int main() {
auto myFuture { async(calculate) };
//auto myFuture { async(launch::async, calculate) };
//auto myFuture { async(launch::deferred, calculate) };
// Do some more work...
// Get the result.
int result { myFuture.get() };
cout << result << endl;
}
正如在此示例中所看到的,std::async() 是异步(在不同线程上)或同步(在同一线程上)执行某些计算并随后检索结果的最简单方法之一。
调用 async() 返回的 future 会在其析构函数中阻塞,直到结果可用。这意味着,如果调用 async() 而不捕获返回的 future,则 async() 调用实际上会变成阻塞调用!例如,以下行同步调用calculate():
async(calculate);
该语句发生的情况是 async() 创建并返回一个 future。这个 future 没有被捕获,所以它是一个临时的 future。因为它是一个临时的 future,所以它的析构函数在该语句的末尾被调用,并且该析构函数将阻塞,直到结果可用。
异常处理
使用 future 的一大优点是它们可以在线程之间传输异常。对 future 调用 get() 要么返回计算结果,要么重新抛出链接到 future 的 promise 中存储的任何异常。当使用 packaged_task 或 async() 时,从启动函数引发的任何异常都会自动存储在 promise 中。如果直接使用 std::promise 作为 promise,则可以调用 set_exception() 在其中存储异常。这是使用 async() 的示例:
int calculate() {
throw runtime_error { "Exception thrown from calculate()." };
}
int main() {
// Use the launch::async policy to force asynchronous execution.
auto myFuture { async(launch::async, calculate) };
// Do some more work...
// Get the result.
try {
int result { myFuture.get() };
cout << result << endl;
} catch (const exception& ex) {
cout << "Caught exception: " << ex.what() << endl;
}
}
std::shared_future
std::future<T> 仅要求 T 是可移动构造的。当你对 future<T> 调用 get() 时,结果将从 future 移出并返回给你。这意味着只能在 future<T> 上调用 get() 一次。
如果你希望能够多次调用 get() ,甚至从多个线程调用 get() ,那么你需要使用std::shared_future<T>
,在这种情况下 T 需要是可复制构造的。可以使用 std::future::share() 或将 future 传递给 shared_future 构造函数来创建 share_future。请注意,future 是不可复制的,因此必须将其移动至shared_future 构造函数中。
shared_future 可用于一次唤醒多个线程。例如,下面的代码定义了两个在不同线程上异步执行的 lambda 表达式。每个 lambda 表达式所做的第一件事就是为各自的 promise 设置一个值,以表明它们已经开始。然后他们都调用 signalFuture 上的 get() ,该方法会阻塞,直到将来有一个参数可用为止,之后他们继续执行。每个 lambda 表达式通过引用捕获各自的 promise,并通过值捕获 signalFuture,因此两个 lambda 表达式都有 signalFuture 的副本。主线程使用 async() 在不同线程上异步执行两个 lambda 表达式,等待两个线程都启动,然后在 signalPromise 中设置参数,从而唤醒两个线程。
promise<void> thread1Started, thread2Started;
promise<int> signalPromise;
auto signalFuture { signalPromise.get_future().share() };
//shared_future<int> signalFuture { signalPromise.get_future() };
auto function1 { [&thread1Started, signalFuture] {
thread1Started.set_value();
// Wait until parameter is set.
int parameter { signalFuture.get() };
// ...
} };
auto function2 { [&thread2Started, signalFuture] {
thread2Started.set_value();
// Wait until parameter is set.
int parameter { signalFuture.get() };
// ...
} };
// Run both lambda expressions asynchronously.
// Remember to capture the future returned by async()!
auto result1 { async(launch::async, function1) };
auto result2 { async(launch::async, function2) };
// Wait until both threads have started.
thread1Started.get_future().wait();
thread2Started.get_future().wait();
// Both threads are now waiting for the parameter.
// Set the parameter to wake up both of them.
signalPromise.set_value(42);
线程池
你可以创建一个可根据需要使用的线程池,而不是在程序的整个生命周期中动态创建和删除线程。这种技术通常用在想要在线程中处理某种事件的程序中。在大多数环境中,理想的线程数量等于处理核心的数量。如果线程数多于核心数,则必须挂起线程以允许其他线程运行,这最终会增加开销。请注意,虽然理想的线程数等于核心数,但这仅适用于线程受计算限制且不能因任何其他原因(包括 I/O)而阻塞的情况。当线程可能阻塞时,运行比内核数量更多的线程通常是合适的。在这种情况下确定最佳线程数很困难,并且可能涉及吞吐量测量。
由于并非所有处理都是相同的,因此线程池中的线程接收表示要完成的计算的函数对象或 lambda 表达式作为其输入的一部分并不罕见。
由于线程池中的线程是预先存在的,因此操作系统从池中调度线程运行比根据输入创建线程要高效得多。此外,使用线程池允许管理创建的线程数量,因此,根据平台的不同,可能只有一个线程或数千个线程。
有多个库可实现线程池,包括 Intel 线程构建模块 (TBB)、Microsoft 并行模式库 (PPL) 等。建议为你的线程池使用这样的库,而不是编写自己的实现。如果确实想自己实现线程池,可以采用与对象池类似的方式来完成。
协程
协程是一种可以在执行过程中暂停并在稍后的时间点恢复的函数。任何在其主体中包含以下内容之一的函数都是协程:
- co_await:在等待计算完成时暂停协程的执行。计算完成后恢复执行。
- co_return:从协程返回(协程中不允许仅返回)。此后协程无法恢复。
- co_yield:从协程返回一个值给调用者并暂停协程,随后再次调用协程在暂停的位置继续执行。
一般来说,协程有两种类型:有栈和无栈。有栈协程可以从嵌套调用深处的任何位置挂起。另一方面,无栈协程只能挂在栈顶的帧上。当无栈协程挂起时,仅保存函数体内具有自动存储期限的变量和临时变量;不保存调用栈。因此,无栈协程的内存使用量很小,允许数百万甚至数十亿个协程同时运行。 C++20 仅支持无栈协程。
协程可用于使用同步编程风格来实现异步操作。用例包括以下内容:
- 生成器
- 异步 I/O
- 惰性计算
- 事件驱动的应用程序
C++20 标准只提供了协程的基础构建块,也就是语言层面添加的内容。C++20 标准库没有提供任何标准化的高层协程,比如生成器。有一些第三方库提供了这样的协程,比如cppcoro。微软 Visual C++ 2019 也带了一些高层构造,比如实验性的generator。下面的代码演示了 Visual C++ 2019 std::experimental::generator 协程的使用:
experimental::generator<int> getSequenceGenerator(int startValue, int numberOfValues) {
for (int i { startValue }; i < startValue + numberOfValues; ++i) {
// Print the local time to standard out.
time_t tt { system_clock::to_time_t(system_clock::now()) };
tm t;
localtime_s(&t, &tt);
cout << put_time(&t, "%H:%M:%S") << ": ";
// Yield a value to the caller, and suspend the coroutine.
co_yield i;
}
}
int main() {
auto gen { getSequenceGenerator(10, 5) };
for (const auto& value : gen) {
cout << value << " (Press enter for next value)";
cin.ignore();
}
}
不幸的是,这就是关于协程的全部内容。自己编写协程(例如experimental::generator)相当复杂,而且太高级。我只是想提一下这个概念,以便你知道它的存在,也许未来的 C++ 标准将引入标准化协程。
线程设计和最佳事件
本节简要列出了一些与多线程编程相关的最佳实践。
- 使用并行标准库算法:标准库包含大量算法。从 C++17 开始,超过 60 个支持并行执行。只要有可能,请使用此类并行算法,而不是编写自己的多线程代码。
- 在关闭应用程序之前,请确保所有线程对象都是 unjoinable 的:确保已在所有线程对象上调用 join() 或 detach()。仍然可连接的线程的析构函数将调用 std::terminate(),它会突然终止所有线程和应用程序本身。 C++20 引入了 jthread,它自动加入其析构函数中。
- 最好的同步是不同步:如果设法以这样的方式设计不同的线程,即处理共享数据的所有线程仅从该共享数据读取而不写入它,或者只写入从不读取的部分,那么多线程编程就会变得容易得多通过其他线程。在这种情况下,不需要任何同步,并且不会出现数据争用或死锁等问题。
- 尝试使用单线程所有权模式:这意味着一个数据块一次由不超过一个线程拥有。拥有该数据意味着不允许其他线程读取或写入该数据。当线程完成数据处理后,数据可以传递给另一个线程,该线程现在拥有数据的唯一且完整的责任/所有权。在这种情况下不需要同步。(参考 Golang 的 channel,通过通信来共享数据而不是通过共享数据来通信)
- 尽可能使用原子类型和操作:原子类型和原子操作可以更轻松地编写无数据争用和无死锁的代码,因为它们会自动处理同步。如果原子类型和操作在多线程设计中是不可能的,并且需要共享数据,则必须使用某种同步机制(例如互斥)来确保正确的同步。
- 使用锁来保护可变共享数据:如果需要多个线程可以写入的可变共享数据,并且不能使用原子类型和操作,则必须使用锁机制来确保不同线程之间的读写同步。
- 尽快释放锁:当需要使用锁保护共享数据时,请确保尽快释放锁。当一个线程持有锁时,它会阻塞等待同一锁的其他线程,这可能会损害性能。
- 使用 RAII 锁对象:使用 lock_guard、unique_lock、shared_lock 或scoped_lock RAII 类在正确的时间自动释放锁。
- 不要手动获取多个锁,而是使用 std::lock()、try_lock() 或 scoped_lock:如果多个线程需要获取多个锁,则必须在所有线程中以相同的顺序获取它们,以防止死锁。你应该使用通用的 std::lock() 或 try_lock() 函数或 scoped_lock 类来获取多个锁。
- 使用多线程感知分析器:这有助于查找多线程应用程序中的性能瓶颈,并查明多个线程是否确实利用了系统中的所有可用处理能力。多线程感知分析器的一个示例是 Microsoft Visual Studio 某些版本中的分析器。
- 了解调试器的多线程支持功能:大多数调试器至少对调试多线程应用程序提供基本支持。你应该能够获取应用程序中所有正在运行的线程的列表,并且应该能够切换到这些线程中的任何一个来检查其调用堆栈。例如,你可以使用它来检查死锁,因为可以准确地看到每个线程正在做什么。
- 使用线程池而不是动态创建和销毁大量线程:如果动态创建和销毁大量线程,性能会下降。在这种情况下,最好使用线程池来重用现有线程。
- 使用更高级别的多线程库:C++ 标准目前仅提供用于编写多线程代码的基本构建块。正确使用它们并非易事。如果可能,请使用更高级别的多线程库,例如 Intel 线程构建模块 (TBB)、Microsoft 并行模式库 (PPL) 等,而不是重新造轮子。多线程编程很难正确并且容易出错。通常,你的轮子可能不像你想象的那么圆。