C++线程同步之互斥锁
文章目录
C++11 中的互斥锁
C++11引入了更现代的多线程编程支持,包括线程库和相关的同步原语。互斥锁(mutex)是用来确保同一时刻只有一个线程能够访问共享资源的一种机制,防止多个线程同时访问共享资源引起的竞态条件。
C++11标准中,互斥锁的实现主要通过以下类来提供:
std::mutex
:std::mutex
是最基本的互斥锁类型,用来保证在同一时刻只有一个线程能够锁住它,其他线程在尝试锁时会被阻塞,直到该互斥锁被解锁。std::recursive_mutex
:std::recursive_mutex
是一种递归互斥锁,允许同一线程多次锁住同一个互斥锁,而不会发生死锁。通常适用于需要多次进入同一临界区的场景。std::timed_mutex
:std::timed_mutex
提供了带有超时机制的互斥锁。线程在尝试获取互斥锁时,可以设置一个超时时间,若超过该时间仍未获得锁,则会放弃请求。std::shared_mutex
:std::shared_mutex
允许多个线程以共享方式读访问同一资源,但在写访问时只能有一个线程独占资源。适用于读多写少的场景。
1. std::mutex
std::mutex
是最基本的互斥锁,主要用于保护共享资源,使得多个线程不能同时访问资源。通常通过 lock()
和 unlock()
来进行显式的加锁和解锁,或者使用 std::lock_guard
或 std::unique_lock
来实现自动锁定和解锁。
使用场景:
- 适用于普通的互斥保护场景,保证每次只有一个线程能够访问某个资源。
示例代码:
#include <iostream>
#include <mutex>
#include <thread>
std::mutex mtx;
void print(int i){
std::lock_guard<std::mutex> lock(mtx); // t1 和 t2 互斥访问
std::cout<<"thread : "<<i<<std::endl;
}
int main(){
std::thread t1(print,1);
std::thread t2(print,2);
t1.join();
t2.join();
return 0;
}
===========输出===============
thread : 1
thread : 2
注意事项:
- 尽量减少临界区的代码量:加锁的代码部分要尽量精简,减少锁持有的时间,避免阻塞其他线程。
- 使用
std::lock_guard
或std::unique_lock
:避免显式调用lock()
和unlock()
,因为手动解锁可能出现错误。
2. std::recursive_mutex
std::recursive_mutex
是一种递归互斥锁,允许同一线程多次加锁同一个锁,而不会发生死锁。适用于需要多次进入同一临界区的场景。
使用场景:
- 适用于递归调用或需要在同一线程内多次获取同一锁的情况。
代码示例:
#include <iostream>
#include <mutex>
#include <thread>
std::recursive_mutex mtx;
void recursiveFunction(int leve1){
if (leve1) {
std::lock_guard<std::recursive_mutex> lock(mtx);
std::cout << "recursiveFunction(" << leve1 << ")\n";
recursiveFunction(leve1 - 1);
}
}
int main(){
std::thread t1(recursiveFunction,3);
t1.join();
return 0;
}
==============输出=============
recursiveFunction(3)
recursiveFunction(2)
recursiveFunction(1)
注意事项:
- 避免滥用递归锁:递归锁只应在需要递归的情况下使用,过度使用可能导致程序的复杂性增加。
- 不要无限递归:递归调用时一定要有终止条件,否则可能会导致死锁或栈溢出。
- 性能问题:递归锁的性能开销高于普通锁,因为它需要维护更多的状态信息。
3. std::timed_mutex
std::timed_mutex
提供了带有超时机制的互斥锁,允许线程在获取锁时指定一个超时。如果在指定时间内未能获得锁,则返回失败,防止线程一直阻塞。
std::timed_mutex
比std::_mutex
多了两个成员函数:try_lock_for()
和try_lock_until()
try_lock_for
函数是当线程获取不到互斥锁资源的时候,让线程阻塞一定的时间长度
try_lock_until
函数是当线程获取不到互斥锁资源的时候,让线程阻塞到某一个指定的时间点
关于两个函数的返回值:当得到互斥锁的所有权之后,函数会马上解除阻塞,返回true,如果阻塞的时长用完或者到达指定的时间点之后,函数也会解除阻塞,返回false
使用场景:
- 适用于需要等待一定时间后自动放弃获取锁的场景,例如在多线程竞争资源时,需要避免死锁或长时间阻塞。
代码示例:
#include <iostream>
#include <mutex>
#include <thread>
#include <chrono>
std::timed_mutex mtx;
void tryLockWithTimeout(int i) {
if (mtx.try_lock_for(std::chrono::milliseconds(100))) { // 尝试获取锁,最多等待100毫秒 , 超时则返回false
std::cout << "Thread " << i << " acquired lock.\n";
std::this_thread::sleep_for(std::chrono::seconds(5)); // 模拟一些工作
mtx.unlock();
} else {
std::cout << "Thread " << i << " failed to acquire lock within timeout.\n";
}
}
int main(){
std::thread t1(tryLockWithTimeout, 1);
std::thread t2(tryLockWithTimeout, 2);
t1.join();
t2.join();
return 0;
}
=======================输出=========================
Thread 1 acquired lock.
Thread 2 failed to acquire lock within timeout.
注意事项:
- 合理设置超时时间:超时时间要根据业务需求来设置,过短可能导致线程频繁失败,过长可能丧失超时机制的意义。
- 避免频繁失败:如果多个线程频繁获取锁失败,可能导致性能下降。可以考虑增加重试逻辑。
4. std::shared_mutex
std::shared_mutex
是 C++17 引入的一种共享互斥锁,允许多个线程同时读访问共享资源,但在写访问时,只有一个线程能够访问资源。它适用于读多写少的场景。
使用场景:
- 适用于读多写少的场景,多个线程可以同时读取数据,但当某个线程需要写数据时,必须独占访问资源。
#include <iostream>
#include <thread>
#include <shared_mutex>
#include <vector>
#include <mutex>
std::shared_mutex smtx;
std::vector<int> data;
void readData(int id) {
std::shared_lock<std::shared_mutex> lock(smtx); // 共享锁
std::cout << "Thread " << id << " is reading data.\n";
if (data.empty()) {
return;
}
for (int i = 0; i < data.size(); ++i) {
std::cout << data[i] << " ";
}
std::cout << "\n";
}
void writeData(int id, int value) {
std::unique_lock<std::shared_mutex> lock(smtx); // 独占锁
std::cout << "Thread " << id << " is writing data." << " value: " << value << "\n";
data.push_back(value);
}
int main() {
std::thread threads[20];
for (int i = 0; i<20; ++i) {
int randomValue = rand() % 100;
if (i%2 == 0) {
threads[i] = std::thread(writeData, i, randomValue);
} else {
threads[i] = std::thread(readData, i);
}
}
for (int i = 0; i < 20; ++i) {
threads[i].join();
}
return 0;
}
===========输出==============
Thread 0 is writing data. value: 83
Thread 1 is reading data.
83
Thread 3 is reading data.
83
Thread 2 is writing data. value: 77
Thread 5 is reading data.
83 77
Thread 4 is writing data. value: 93
Thread 6 is writing data. value: 86
Thread 7 is reading data.
83 77 93 86
Thread 8 is writing data. value: 49
Thread 9 is reading data.
83 77 93 86 49
Thread 10 is writing data. value: 62
Thread 11 is reading data.
83 77 93 86 49 62
Thread 12 is writing data. value: 90
Thread 13 is reading data.
83 77 93 86 49 62 90
Thread 14 is writing data. value: 63
Thread 15 is reading data.
83 77 93 86 49 62 90 63
Thread 17 is reading data.
83 77 93 86 49 62 90 63
Thread 16 is writing data. value: 40
Thread 18 is writing data. value: 72
Thread 19 is reading data.
83 77 93 86 49 62 90 63 40 72
注意事项:
- 读写冲突:只有在没有其他线程正在写数据时,才允许多个线程并行读数据。写操作会阻塞所有读操作,因此在高并发环境中要注意读写的平衡。
- 适用场景:读多写少的场景是
std::shared_mutex
最佳的使用场景,能够有效提高性能。
总结
1. std::mutex
- 使用场景:保护共享资源,避免多个线程同时访问。
- 注意点:避免手动加锁解锁带来的错误,推荐使用
std::lock_guard
或std::unique_lock
。
2. std::recursive_mutex
- 使用场景:适用于递归调用的场景,允许同一线程多次锁定同一资源。
- 注意点:避免滥用递归锁,确保递归调用的终止条件。
3. std::timed_mutex
- 使用场景:需要超时控制的场景,避免线程长时间阻塞。
- 注意点:合理设置超时时间,避免线程频繁失败。
4. std::shared_mutex
- 使用场景:读多写少的场景,多个线程可以共享读取资源,写操作需要独占。
- 注意点:读写冲突要避免,保证写操作能够及时完成。