1 std::mutex 的基础概念
1.1 std::mutex 的定义与声明
std::mutex 是 C++11 标准库中的一个互斥量(mutex)类,用于保护共享资源的并发访问。在多线程环境中,当多个线程试图同时访问和修改同一资源时,可能会发生数据竞争和不一致的问题。std::mutex 提供了一种机制,使得同一时间只有一个线程能够访问被保护的资源。
定义:
std::mutex 是一个类,它定义在 <mutex> 头文件中。要使用 std::mutex,需要包含这个头文件。这个类提供了互斥锁的基本功能,以及管理锁状态的机制。
声明:
声明一个 std::mutex 对象非常简单。只需要在代码中创建一个该类型的变量即可。例如:
#include <mutex> std::mutex mtx; // 声明一个互斥量对象 mtx
在上面的代码中,mtx 是一个 std::mutex 类型的对象。这个对象在创建时处于解锁状态,表示它当前没有锁定任何资源。
特点:
- 互斥性:std::mutex 提供了互斥锁的基本功能,即同一时间只允许一个线程访问被保护的共享资源。当一个线程获取了互斥锁后,其他试图获取该锁的线程会被阻塞,直到该线程释放锁。
- 不可复制性:std::mutex 对象是不可复制的。这是为了确保互斥锁的所有权在不同线程之间正确传递,防止同一个互斥锁对象被多个线程同时持有。
- 可移动性:虽然 std::mutex 对象不可复制,但它是可移动的。这意味着可以通过移动语义来转移互斥锁的所有权。这在某些情况下是有用的,比如当需要在容器或智能指针中存储互斥锁时。
成员函数:
std::mutex 类提供了几个成员函数来管理锁的状态:
- 构造函数:创建一个新的 std::mutex 对象,该对象在创建时处于解锁状态。
- lock():尝试锁定互斥量。如果互斥量当前没有被锁定,则锁定它并立即返回。如果互斥量已经被另一个线程锁定,则当前线程会被阻塞,直到互斥量变得可用并被当前线程锁定。
- unlock():解锁之前由同一线程锁定的互斥量。如果互斥量没有被当前线程锁定,则行为是未定义的。
- try_lock()(在 std::timed_mutex 或 std::recursive_timed_mutex 中提供):尝试锁定互斥量,如果互斥量已被另一个线程锁定,则不会阻塞当前线程,而是立即返回表示是否成功锁定的结果。
使用 std::mutex 时,通常建议与 std::lock_guard 或 std::unique_lock 结合使用,它们提供了 RAII(Resource Acquisition Is Initialization)风格的锁管理,可以确保在离开作用域时自动释放锁,从而避免忘记解锁而导致的问题。
1.2 std::mutex 的状态与行为
状态:
std::mutex 有两种基本状态:锁定状态和未锁定状态。
- 未锁定状态:这是 std::mutex 对象在创建后的初始状态,也是每次调用 unlock() 方法后的状态。在未锁定状态下,任何线程都可以尝试获取互斥锁。
- 锁定状态:当一个线程成功调用 lock() 方法后,std::mutex 对象会进入锁定状态。在锁定状态下,其他试图调用 lock() 方法的线程将被阻塞,直到互斥锁被当前持有者释放(即调用 unlock() 方法)。
行为:
std::mutex 的行为主要通过其成员函数来体现,这些函数允许线程获取和释放互斥锁。
(1)构造函数和析构函数:
- 构造函数会初始化 std::mutex 对象,将其设置为未锁定状态。
- 析构函数会自动释放互斥锁(如果它被锁定的话),确保在对象销毁时不会发生悬挂锁或死锁。
(2)lock():
- 当一个线程调用 lock() 方法时,如果 std::mutex 对象当前处于未锁定状态,该线程将成功获取互斥锁,并将互斥锁状态设置为锁定。
- 如果 std::mutex 对象已经被另一个线程锁定,调用 lock() 的线程将被阻塞,直到互斥锁被释放。
(3)unlock():
- 调用 unlock() 方法会释放由当前线程持有的 std::mutex 对象。这会将互斥锁的状态从锁定更改为未锁定,并允许其他线程获取该锁。
- 如果当前线程没有锁定 std::mutex 对象,调用 unlock() 方法是未定义行为,通常会导致程序崩溃。
(4)try_lock()(在 std::timed_mutex 或其他相关类型中提供):
- 尝试获取互斥锁,而不会阻塞当前线程。如果互斥锁当前可用(即处于未锁定状态),则获取锁并返回 true;否则,不阻塞并立即返回 false。
注意事项:
- std::mutex 不支持递归锁定,即同一个线程不能多次锁定同一个 std::mutex 对象而没有相应的解锁操作。这样做会导致未定义行为,通常是死锁。
- 线程在持有 std::mutex 锁时不应执行可能导致阻塞的操作(如 I/O 操作或等待用户输入),因为这可能会不必要地延长其他线程的等待时间。
- 使用 std::mutex 时应始终确保配对使用 lock() 和 unlock(),以避免资源泄漏或死锁。为此,建议使用 std::lock_guard 或 std::unique_lock,它们会在构造时自动锁定互斥量,并在析构时自动解锁,从而简化锁的管理。
2 std::mutex 的使用
2.1 lock() 与 unlock() 函数
std::mutex 类中的 lock() 和 unlock() 函数是控制互斥量(mutex)锁定和解锁状态的关键方法。在多线程编程中,这两个函数用于确保同一时间只有一个线程能够访问特定的共享资源,从而避免数据竞争和不一致的问题。
lock() 函数
lock() 函数尝试获取互斥量的所有权。如果互斥量当前没有被其他线程锁定,那么调用 lock() 的线程会成功获取互斥量,并继续执行后续的代码。如果互斥量已经被其他线程锁定,那么调用 lock() 的线程会被阻塞,直到互斥量变为可用状态。
unlock() 函数
unlock() 函数用于释放先前通过 lock() 获取的互斥量。调用 unlock() 之后,其他线程就可以尝试获取这个互斥量了。重要的是,只有锁定了互斥量的线程才能调用 unlock(),否则会导致未定义行为,通常会导致程序崩溃。
示例
下面是一个简单的示例,展示了如何使用 std::mutex 的 lock() 和 unlock() 函数来保护共享资源的访问:
#include <iostream>
#include <thread>
#include <mutex> std::mutex mtx; // 全局互斥量
int shared_data = 0; // 共享数据 void increment() {mtx.lock(); // 尝试获取互斥量 for (size_t i = 0; i < 5; i++){++shared_data; // 访问和修改共享数据 std::cout << "Thread " << std::this_thread::get_id() << " incremented shared_data to " << shared_data << std::endl;}mtx.unlock(); // 释放互斥量
}int main() {std::thread t1(increment);std::thread t2(increment);t1.join();t2.join();std::cout << "Final value of shared_data: " << shared_data << std::endl;return 0;
}
上面代码的输出为:
Thread 4936 incremented shared_data to 1
Thread 4936 incremented shared_data to 2
Thread 4936 incremented shared_data to 3
Thread 4936 incremented shared_data to 4
Thread 4936 incremented shared_data to 5
Thread 10384 incremented shared_data to 6
Thread 10384 incremented shared_data to 7
Thread 10384 incremented shared_data to 8
Thread 10384 incremented shared_data to 9
Thread 10384 incremented shared_data to 10
Final value of shared_data: 10
这个示例定义了一个全局的 std::mutex 对象 mtx 和一个全局的整数 shared_data 作为共享资源。increment 函数尝试增加 shared_data 的值。在修改 shared_data 之前,函数首先调用 mtx.lock() 来获取互斥量。如果互斥量已经被另一个线程锁定,当前线程将会等待。一旦获取到互斥量,线程就可以安全地修改 shared_data 了。修改完成后,线程调用 mtx.unlock() 来释放互斥量,这样其他线程就可以获取它并修改 shared_data 了。
2.2 调用线程的行为分析
(1)互斥量未被锁住时
当互斥量(mutex)未被锁住时,任何线程都可以尝试调用lock()函数来获取互斥量的所有权。一旦线程成功获取到互斥量,它将拥有对共享资源的独占访问权,其他线程尝试调用lock()将阻塞,直到当前线程释放互斥量。
示例代码:
#include <iostream>
#include <thread>
#include <mutex> std::mutex mtx; // 全局互斥量 void safe_increment() { mtx.lock(); // 互斥量未被锁住,线程将获取锁 static int counter = 0; ++counter; std::cout << "Counter: " << counter << std::endl; mtx.unlock(); // 释放锁
} int main()
{ std::thread t1(safe_increment); std::thread t2(safe_increment); t1.join(); t2.join(); return 0;
}
上面代码的输出为:
Counter: 1
Counter: 2
在这个例子中,当 t1 和 t2 线程尝试调用 safe_increment 函数时,互斥量 mtx 是未被锁住的。因此,每个线程都会先获取互斥量,然后安全地增加 counter 的值,最后释放互斥量。这样,就不会发生两个线程同时修改 counter 的情况,从而避免了数据竞争。
(2)互斥量被其他线程锁住时
当互斥量已经被其他线程锁住时,任何尝试调用 lock()的线程都会被阻塞,直到拥有锁的线程调用 unlock() 释放互斥量。这种机制确保了同一时间只有一个线程能够访问受保护的共享资源。
示例代码:
#include <iostream>
#include <thread>
#include <mutex>
#include <chrono> std::mutex mtx; void long_operation() { mtx.lock(); std::cout << "Thread " << std::this_thread::get_id() << " is doing a long operation." << std::endl; std::this_thread::sleep_for(std::chrono::seconds(2)); // 模拟长时间操作 mtx.unlock();
} void another_operation() { std::cout << "Thread " << std::this_thread::get_id() << " is waiting for the mutex." << std::endl; mtx.lock(); // 如果mtx已被其他线程锁住,这里会阻塞 std::cout << "Thread " << std::this_thread::get_id() << " acquired the mutex." << std::endl; mtx.unlock();
} int main()
{ std::thread t1(long_operation); std::thread t2(another_operation); t1.join(); t2.join(); return 0;
}
上面代码的输出为:
Thread 9672 is doing a long operation.Thread 10244 is waiting for the mutex.Thread 10244 acquired the mutex.
在这个例子中,t1 线程首先获取了互斥量并执行长时间操作。当 t2 线程尝试获取互斥量时,由于 mtx 已被 t1 线程锁住,t2 线程会被阻塞,直到 t1 线程释放互斥量。
(3)互斥量被当前线程锁住时(死锁情况)
当互斥量被当前线程锁住,并且该线程再次尝试锁住同一个互斥量时,会发生死锁。死锁是指两个或更多线程无限期地等待一个资源,而该资源却被另一个线程持有,导致所有线程都无法继续执行。在 C++ 中,如果一个线程试图对一个已经被它锁住的互斥量再次调用 lock(),那么行为是未定义的,通常会导致程序崩溃或死锁。
为了避免死锁,应该确保每个线程在完成对共享资源的访问后都释放互斥量,并且不要在已经拥有锁的情况下再次尝试获取同一个锁。如果确实需要递归锁定,应该使用 std::recursive_mutex 而不是 std::mutex。
示例代码(错误用法,会导致死锁):
#include <iostream>
#include <thread>
#include <mutex>
#include <chrono> std::mutex mtx; void deadlock_risk() { mtx.lock(); // ... do some work ... // 错误:尝试再次锁住已经被当前线程锁住的互斥量 mtx.lock(); // 这将导致未定义行为,通常是死锁
}int main()
{ std::thread t(deadlock_risk); t.join(); return 0;
}
3 RAII 风格的锁管理
3.1 std::lock_guard 的使用
std::lock_guard 是 C++11 引入的一个模板类,用于在作用域内自动管理互斥量(mutex)的加锁和解锁操作。它的主要目标是简化互斥量的管理,避免由于异常或忘记解锁而导致的问题。通过使用 std::lock_guard,可以确保在代码块结束时,互斥量会被自动解锁,即使发生异常也是如此。
下面是 std:: lock_guard 的使用方法和一些关键点:
(1)包含头文件
首先,需要包含 <mutex> 头文件来使用 std::lock_guard。
#include <mutex>
(2)创建互斥量
在使用 std::lock_guard 之前,需要创建一个互斥量(mutex)对象。
std::mutex mtx; // 全局或局部变量
(3)使用 std::lock_guard
创建一个 std::lock_guard 对象时,它会尝试锁定传入的互斥量。如果互斥量已经被其他线程锁定,那么当前线程会阻塞,直到互斥量被释放。
当 std::lock_guard 对象被创建时,它的构造函数会自动调用 lock() 函数来锁定互斥量。当 std::lock_guard 对象离开其作用域(例如,在函数返回或块结束时)时,它的析构函数会自动调用 unlock() 函数来解锁互斥量。
下面是一个简单的使用示例:
void safe_increment() { std::lock_guard<std::mutex> lock(mtx); // 锁定互斥量 // 在这里执行对共享资源的访问 static int counter = 0; ++counter; std::cout << "Counter: " << counter << std::endl; // 无需手动解锁,std::lock_guard会在离开作用域时自动解锁
}
在上面的示例中,safe_increment 函数使用 std::lock_guard 来确保对 counter 的访问是线程安全的。在 std::lock_guard 对象 lock 的生命周期内,互斥量 mtx 被锁定,从而防止其他线程同时访问 counter。当 lock 对象离开其作用域时(即函数结束时),互斥量自动解锁。
(4)异常安全性
std::lock_guard 的一个重要优点是它的异常安全性。即使在加锁后的代码块中抛出异常,std::lock_guard 的析构函数仍然会确保互斥量被正确解锁。这使得使用 std::lock_guard 的代码更加健壮和易于管理。
(5)注意事项
- std::lock_guard 对象不能被复制或移动,因此它们通常作为局部变量在需要保护的代码块中使用。
- 一旦 std::lock_guard 对象被创建并锁定了一个互斥量,就不能再手动解锁该互斥量。解锁操作会在 std::lock_guard 对象析构时自动发生。
- 如果需要更复杂的锁管理(例如,同时锁定多个互斥量),可能需要使用其他机制,如 std::lock 函数或 std::unique_lock。
3.2 std::unique_lock 的使用与优势
std::unique_lock 是 C++11 引入的一个模板类,它提供了一种灵活的方式来管理互斥量(mutex)的锁定和解锁操作。与 std::lock_guard 相比,std::unique_lock 提供了更多的控制选项,使得在复杂的并发编程场景中能够更精细地管理锁。
下面是 std:: unique_lock 的使用方法和一些关键点:
(1)包含头文件
首先,需要包含 <mutex> 头文件来使用 std::unique_lock。
#include <mutex>
(2)创建互斥量
在使用std::unique_lock之前,你需要创建一个互斥量对象。
std::mutex mtx; // 互斥量对象
(3)创建std::unique_lock对象
将互斥量对象传递给 std::unique_lock 的构造函数来创建一个 std::unique_lock 对象。这会自动尝试锁定互斥量。
std::unique_lock<std::mutex> lock(mtx); // 锁定互斥量
此时,互斥量mtx被lock对象锁定,其他线程无法获得该锁。
(4)手动控制锁定和解锁
与 std::lock_guard 不同,std::unique_lock 允许在其生命周期内手动控制锁定和解锁。
lock.lock(); // 手动锁定互斥量
// 临界区代码
lock.unlock(); // 手动解锁互斥量
如果希望在某个特定的作用域内锁定互斥量,并在离开作用域时自动解锁,可以将 lock.lock() 和 lock.unlock() 调用放在该作用域内。
(5)自动管理
尽管提供了手动控制的功能,但 std::unique_lock 仍然支持自动管理锁定和解锁。当 std::unique_lock 对象离开其作用域(例如,在大括号结束时)时,其析构函数会自动调用 unlock() 来解锁互斥量。
{ std::unique_lock<std::mutex> lock(mtx); // 自动锁定 // 临界区代码 // 当离开这个作用域时,lock对象会自动解锁互斥量
}
(6)独特功能
除了基本的锁定和解锁功能外,std::unique_lock还提供了一些独特的功能:
延迟锁定: 可以创建一个 std::unique_lock 对象,但不立即锁定互斥量。这可以通过在构造函数中使用 std::defer_lock 标签来实现。
std::unique_lock<std::mutex> lock(mtx, std::defer_lock); // 创建但不锁定
lock.lock(); // 稍后手动锁定
尝试锁定: 使用 try_lock() 成员函数尝试锁定互斥量,如果互斥量已被其他线程锁定,则不会阻塞当前线程,而是立即返回。
if (lock.try_lock()) { // 成功锁定,执行临界区代码 lock.unlock(); // 记得解锁
} else { // 未能锁定,执行其他操作
}
与条件变量配合使用: std::unique_lock 经常与 std::condition_variable 一起使用,以实现线程间的同步。std::condition_variable 的某些成员函数(如 wait()和 wait_for())需要 std::unique_lock 作为参数。
std::condition_variable cv;
std::mutex mtx;
std::unique_lock<std::mutex> lock(mtx);
cv.wait(lock, []{ /* 等待条件成立 */ }); // 使用unique_lock作为参数
(7)总结
std::unique_lock 为 C++ 的并发编程提供了灵活且强大的锁管理功能。它支持自动和手动锁定和解锁,允许延迟锁定和尝试锁定,并能与条件变量配合使用,从而在处理复杂的并发情况时提供了更多的选择和便利。选择使用 std::unique_lock 还是 std::lock_guard 取决于具体的应用场景和需求。在需要更多控制和灵活性时,std::unique_lock 通常是更好的选择。
4 定时锁 std::timed_mutex
std::timed_mutex 是 C++11 标准库中引入的一种特殊的互斥量(mutex)类型,它允许线程在尝试获取锁时设置一个超时时间。这种机制在多线程编程中特别有用,因为它可以防止线程因长时间等待锁而导致的性能下降或死锁情况。
std::timed_mutex的主要特点
- 超时锁定:与普通的 std::mutex 不同,std::timed_mutex 提供了 try_lock_for 和 try_lock_until 两个成员函数,允许线程在指定的时间段内尝试获取锁。如果在这个时间段内未能获取到锁,线程可以选择放弃并继续执行其他操作,而不是无限期地等待。
- 灵活性:通过设定超时时间,std::timed_mutex 为线程提供了更多的灵活性。线程可以根据需要调整超时时间,以适应不同的并发场景和性能要求。
- 线程安全:std::timed_mutex 满足定时互斥体(TimedMutex)的所有要求,并确保了线程安全。它的复制构造函数已被删除,以防止不当的复制操作。
std::timed_mutex的成员函数
- try_lock_for(const std::chrono::duration<Rep, Period>& rel_time): 尝试在给定的时间段内获取锁。如果成功获取到锁,则返回true;否则,在超时后返回false。
- try_lock_until(const std::chrono::time_point<Clock, Duration>& abs_time): 尝试在指定的时间点之前获取锁。如果成功获取到锁,则返回true;否则,在到达指定时间点后返回false。
使用示例
下面是一个简单的示例,展示了如何使用std::timed_mutex:
#include <iostream>
#include <thread>
#include <mutex>
#include <chrono> std::timed_mutex mtx; // 创建一个 timed_mutex 对象 void worker() { if (mtx.try_lock_for(std::chrono::milliseconds(100))) { // 尝试获取锁,超时时间为100毫秒 std::cout << "Thread " << std::this_thread::get_id() << " acquired the mutex." << std::endl; std::this_thread::sleep_for(std::chrono::seconds(1)); // 模拟临界区操作,耗时1秒 mtx.unlock(); // 解锁 } else { std::cout << "Thread " << std::this_thread::get_id() << " failed to acquire the mutex within the timeout." << std::endl; }
} int main()
{ std::thread t1(worker); std::thread t2(worker); t1.join(); t2.join(); return 0;
}
上面代码的输出为:
Thread 1032 acquired the mutex.
Thread 10300 failed to acquire the mutex within the timeout.
这个示例创建了一个 std::timed_mutex 对象mtx,并在 worker 函数中尝试使用 try_lock_for 函数获取锁。这里设置了 100 毫秒的超时时间。如果线程在 100 毫秒内成功获取到锁,它将执行临界区代码(这里用 std::this_thread::sleep_for 模拟了一个耗时操作),然后解锁。如果线程在超时时间内未能获取到锁,它将输出一条失败的消息。
5 递归锁 std::recursive_mutex
std::recursive_mutex 是 C++11 标准库中引入的一种特殊类型的互斥量(mutex),它允许同一线程多次获取同一个锁而不会引起死锁。这在某些递归函数或者需要多次锁定同一资源的场景中非常有用。普通的 std::mutex 是不允许同一线程多次锁定的,如果尝试这样做,将会导致未定义行为,通常是死锁。
std::recursive_mutex 的主要特点
- 递归锁定:同一线程可以多次调用 lock() 或 try_lock() 方法而不会导致死锁。每次 lock() 调用都需要有一个对应的 unlock() 调用,以确保最终锁会被完全释放。
- 线程安全:与所有互斥量一样,std::recursive_mutex 是线程安全的,可以在多线程环境中安全使用。
- 性能考虑:由于 std::recursive_mutex 比 std::mutex 提供了更多的功能,因此可能在某些情况下会有稍微的性能开销。在设计并发系统时,应尽量避免不必要的递归锁定,因为它可能导致更复杂的同步问题。
std::recursive_mutex 的成员函数
- lock(): 尝试锁定互斥量。如果互斥量已被锁定,则调用线程将阻塞,直到锁被释放。
- unlock(): 解锁互斥量。只有锁定它的线程才能解锁它。
- try_lock(): 尝试非阻塞地锁定互斥量。如果互斥量已被锁定,则立即返回并指示失败。
使用示例
下面是一个简单的示例,展示了如何使用 std::recursive_mutex:
#include <iostream>
#include <thread>
#include <mutex> std::recursive_mutex mtx; // 创建一个递归互斥量对象 // 一个递归函数,需要多次锁定相同的互斥量
void recursive_function(int depth) { mtx.lock(); // 锁定互斥量 std::cout << "Thread " << std::this_thread::get_id() << " entered recursive function at depth " << depth << std::endl; if (depth > 0) { recursive_function(depth - 1); // 递归调用,再次锁定互斥量 } std::cout << "Thread " << std::this_thread::get_id() << " is leaving recursive function at depth " << depth << std::endl; mtx.unlock(); // 解锁互斥量
} int main()
{ std::thread t1(recursive_function, 3); // 创建一个线程执行递归函数,深度为3 t1.join(); // 等待线程完成 return 0;
}
上面代码的输出为:
Thread 5968 entered recursive function at depth 3
Thread 5968 entered recursive function at depth 2
Thread 5968 entered recursive function at depth 1
Thread 5968 entered recursive function at depth 0
Thread 5968 is leaving recursive function at depth 0
Thread 5968 is leaving recursive function at depth 1
Thread 5968 is leaving recursive function at depth 2
Thread 5968 is leaving recursive function at depth 3
这个示例创建了一个 std::recursive_mutex 对象 mtx,并在 recursive_function 函数中多次调用 lock() 和 unlock()。由于 recursive_function 是一个递归函数,它会多次尝试锁定同一个互斥量,而不会导致死锁。如果这里使用的是 std::mutex 而不是 std::recursive_mutex,那么程序将会出现未定义行为,很可能是死锁。
6 std::call_once
std::call_once 是 C++ 标准库中的一个非常有用的工具,用于确保某个函数在多线程环境中只被调用一次。这在初始化资源或执行只需要运行一次的代码时非常有用。
工作原理
std::call_once 的工作原理基于一个 std::once_flag 对象。当调用 std::call_once 时,它首先检查 std::once_flag 的状态。如果标志指示函数已经被调用过,那么 std::call_once 将立即返回而不执行任何操作。如果标志指示函数尚未被调用,那么 std::call_once 将执行提供的函数,并在执行完成后设置标志,以防止该函数再次被调用。
函数原型
函数原型如下:
template<class Callable, class... Args>
void call_once(std::once_flag& flag, Callable&& f, Args&&... args);
- std::once_flag& flag:这是一个引用到 std::once_flag 对象的参数,用于跟踪函数是否已经被调用过。
- Callable&& f:这是要调用的函数或可调用对象。
- Args&&… args:这是传递给函数f的参数包。
注意事项
- 如果在调用 std::call_once 时,std::once_flag 指示函数f已经调用过,那么 std::call_once 将立即返回,不会再次调用f。
- 如果在调用函数 f 时抛出了异常,那么这个异常将传播给 std::call_once 的调用方,并且 std::once_flag 不会被改变状态(即,仍然表示函数尚未被成功调用)。
- 如果有多个线程同时尝试在 std::once_flag 未设置(即函数未调用过)时调用 std::call_once,那么这些调用将被组成单独全序,并依次执行。也就是说,尽管有多个线程尝试调用,但函数 f 本身只会被执行一次。
示例
下面是一个简单的示例,展示了如何使用 std::call_once 来确保一个函数在多线程环境中只被调用一次:
#include <iostream>
#include <thread>
#include <mutex> std::once_flag once_flag; // 全局的 once_flag 对象 void init() { std::call_once(once_flag, []() { std::cout << "Initialization function called once." << std::endl; // 这里执行只需要执行一次的初始化代码 });
} void worker() { init(); // 调用 init 函数,它将使用 call_once 来确保只初始化一次 // 其他工作代码...
} int main()
{ std::thread threads[5]; // 创建5个线程 for (int i = 0; i < 5; ++i) { threads[i] = std::thread(worker); // 每个线程都执行 worker 函数 } for (auto& th : threads) { th.join(); // 等待所有线程完成 } return 0;
}
上面代码的输出为:
Initialization function called once.
这个示例创建了 5 个线程,每个线程都调用 worker 函数。worker 函数又调用了 init 函数,而 init 函数使用了 std::call_once 来确保初始化函数只被调用一次。尽管有 5 个线程同时运行,但初始化函数只会被打印一次,因为 std::call_once 确保了这一点。
7 std::mutex 的最佳实践
7.1 死锁的产生原因与影响
(1)死锁的产生原因
- 互斥:死锁的首要条件是互斥,即一个资源一次只能被一个线程或进程占用。如果多个线程争夺同一资源,并且在获取资源时无法共享,就可能导致死锁。
- 占有且等待:这是死锁的另一个条件,它要求一个线程在等待其他线程释放资源的同时,自己占有着至少一个资源。这样的情况下,各线程之间就可能形成一个环路,导致死锁。
- 不可抢占:不可抢占要求资源在被占用的情况下无法被强制抢占,只能由占有者主动释放。如果一个线程占有资源后不愿意释放,其他线程就可能因无法获得资源而陷入等待状态,造成死锁。
- 循环等待:最后一个死锁产生的条件是循环等待,即若干线程之间形成了一个循环,每个线程都在等待下一个线程释放资源。这种循环等待会导致程序无法继续执行。
此外,竞争不可抢占资源也是引起死锁的常见原因。通常系统中拥有的不可抢占资源数量不足以满足多个进程运行的需要,使得进程在运行过程中会因争夺资源而陷入僵局,如磁带机、打印机等。
进程推进顺序不当同样会引起死锁。进程在运行过程中,请求和释放资源的顺序不当,会导致死锁。信号量使用不当也会造成死锁。
(2)死锁的影响
- 资源浪费:死锁导致系统中的资源被无效地占用,而无法释放,从而浪费了系统资源。
- 程序停滞:死锁会导致相关进程或线程被永久阻塞,无法继续执行,从而影响系统的正常运行。
- 系统崩溃:在一些情况下,死锁可能导致系统崩溃,从而造成严重的后果。
7.2 避免死锁的策略
以下是使用 std::mutex 避免死锁的一些关键策略:
(1)避免嵌套锁
避免在一个线程中嵌套使用多个互斥锁,因为这可能导致死锁,特别是当锁的获取顺序在不同线程间不一致时。如果确实需要嵌套锁,应确保所有线程都按照相同的顺序请求锁。
(2)锁定时限
使用 std::timed_mutex 或 std::recursive_timed_mutex 等带时限的互斥锁,可以为锁操作设置超时时间。如果线程在指定的时间内未能获取到锁,它可以选择放弃并处理相应的错误,而不是无限期地等待下去,这有助于减少死锁的可能性。
(3)锁的顺序
当多个线程需要访问多个共享资源时,确保所有线程都以相同的顺序请求锁。这有助于防止循环等待条件的发生,从而减少死锁的风险。
(4)避免持有锁执行长时间操作
尽量避免在持有锁的情况下执行长时间的操作或可能阻塞的操作,因为这可能导致其他线程长时间等待锁,从而增加死锁的风险。
(5)使用智能锁
使用 std::lock_guard 或 std::unique_lock 等智能锁,可以确保锁在适当的时候被释放。这些智能锁在构造时自动获取锁,在析构时自动释放锁,从而减少了因忘记释放锁而导致的死锁风险。
(6)检测和恢复
实现死锁检测和恢复机制。这可以通过定期检查线程状态、监控锁的使用情况等方式来实现。当检测到死锁时,可以采取一些措施来打破死锁,如强制释放某些锁或终止某些线程。
(7)使用高级同步原语
考虑使用更高级的同步原语,如 std::condition_variable、std::future 和 std::promise 等,这些原语提供了更灵活的同步机制,有时可以替代直接使用互斥锁,从而减少死锁的风险。