互斥锁
用于保护对共享资源的互斥访问,当有一个线程获得锁时,其他线程需要等待解锁才能访问该资源。
std::mutex //常见互斥锁,用于保护共享资源,确保只有一个线程可以访问该资源。
std::mutex mtx;
void func() {
mtx.lock(); // 访问共享资源
mtx.unlock();
}
int main() {
std::thread t1(func);
std::thread t2(func);
t1.join();
t2.join();
return 0;
}
std::shared_mutex s_mtx; //共享互斥锁,允许多个读线程同时访问共享资源,但只允许一个写线程访问。
void read_func() {
s_mtx.lock_shared(); // 读取共享资源
s_mtx.unlock_shared();
}
void write_func() {
s_mtx.lock(); // 修改共享资源
s_mtx.unlock();
}
int main() {
std::thread t1(read_func);
std::thread t2(read_func);
std::thread t3(write_func);
t1.join();
t2.join();
t3.join();
return 0;
}
递归锁
递归锁可以被同一个线程多次获得,有助于避免死锁,因为在递归锁未释放之前,其他线程无法获得该锁。
std::recursive_mutex
//是递归锁,允许同一线程多次获得锁,但在释放锁前必须释放锁的次数与获得锁的次数相同
std::recursive_mutex r_mtx;
void func() {
r_mtx.lock();
r_mtx.lock(); // 第二次获得锁
// 访问共享资源
r_mtx.unlock();
r_mtx.unlock(); // 第二次释放锁
}
int main() {
std::thread t1(func);
t1.join();
return 0;
}
读写锁
读写锁允许共享资源的并发读取,但是当一个线程想要写入该资源时,需要获得独占锁,此时其他线程无法读取或写入该资源。
自旋锁
自旋锁的效果类似于互斥锁,但是当线程无法获得锁时,它并不会阻塞,而是一直轮询等待锁的释放。
//使用std::atomic_flag的自旋锁互斥实现
class SpinLock {
public:
SpinLock() { _flag.clear(); }
void lock() {
while (_flag.test_and_set(std::memory_order_acquire));
}
void unlock() {
_flag.clear(std::memory_order_release);
}
private:
std::atomic_flag _flag;
};
void worker(SpinLock &lock, int &counter) {
for (int i = 0; i < 1000000; ++i) {
lock.lock();
++counter;
lock.unlock();
}
}
int main() {
SpinLock lock;
int counter = 0;
std::thread t1(worker, std::ref(lock), std::ref(counter));
std::thread t2(worker, std::ref(lock), std::ref(counter));
t1.join();
t2.join();
std::cout << "Final counter value: " << counter << std::endl; return 0;
}
条件锁
条件锁就是所谓的条件变量,某一个线程因为某个条件为满足时可以使用条件变量使改程序处于阻塞状态。一旦条件满足以“信号量”的方式唤醒一个因为该条件而被阻塞的线程。最为常见就是在线程池中,起初没有任务时任务队列为空,此时线程池中的线程因为“任务队列为空”这个条件处于阻塞状态。一旦有任务进来,就会以信号量的方式唤醒一个线程来处理这个任务
//使用std::condition_variable等待数据
std::mutex mut;
std::queue<data_chunk> data_queue;
std::condition_variable data_cond;
void data_preparation_thread()
{
while(more_data_to_prepare())
{
data_chunk const data=prepare_data();
std::lock_guard<std::mutex> lk(mut);
data_queue.push(data);
data_cond.notify_one();
}
}
void data_processing_thread()
{
while(true)
{
std::unique_lock<std::mutex> lk(mut); //这里使用unique_lock是为了后面方便解锁
data_cond.wait(lk,{[]return !data_queue.empty();});
data_chunk data=data_queue.front();
data_queue.pop();
lk.unlock();
process(data);
if(is_last_chunk(data))
break;
}
}
选择依据
选择锁的依据主要是根据互斥量的性能、粒度、可重入性、多核效能等方面做出判断,一般情况下可以先使用互斥锁,如果性能出现瓶颈再考虑其它类型的锁。同时,也要注意锁的粒度和可重入性对程序性能的影响,以及不同锁的使用限制与注意事项。
-
互斥量的性能
互斥量的性能主要取决于两个方面:等待时间和锁的粒度。
等待时间是指当一个线程申请互斥锁时,需要等待的时间长度。
较短的等待时间可以提高锁的性能,因为可以减少线程的阻塞时间。
等待时间的长短通常受系统负载和调度策略等因素影响,因此需要尽可能优化锁操作的代码,以减少线程的等待时间。
-
粒度
锁的粒度是指锁所保护的共享资源的范围大小。
锁的粒度越小,保护的共享资源越少,就可以在并发访问时获得更大的吞吐量。但较小的锁粒度也会导致锁竞争的频繁发生,从而降低程序性能。
-
可重入性
一个线程获得锁之后,可以再次获得这个锁而不会造成死锁等问题。
可重入锁能够提高程序运行的效率,因为重入锁的成本比较低。相反,非可重入锁在性能方面较差,因为每次重复锁定时,都需要额外的开销。
-
多核效能
在多核处理器上,锁对程序性能的影响非常显著,因为多个线程可能会同时尝试访问共享资源,从而产生锁竞争和资源争用。
为了最大化利用多核处理器的性能,可以采用如下策略来优化锁的使用:
-
减小锁粒度:把共享资源的访问粒度尽量减小,从而减少锁的冲突和竞争。例如,可以把大的数据结构拆分成多个小的片段,每个片段都使用不同的锁来保护。
-
使用自旋锁:自旋锁可以避免线程切换和上下文切换的开销,从而提高锁的性能。但是自旋锁应仅在共享资源访问时间非常短且锁竞争非常小的情况下使用。
-
使用读写锁:读写锁允许多个线程同时读取共享资源,但只能有一个线程写入。这样可以有效减少锁冲突和竞争,提高程序性能。
-
使用锁-free算法:锁-free算法可以最大化利用多核处理器的性能,但需要高度的程序并发控制。锁-free算法会把锁和互斥访问所产生的冲突转化为无锁算法中的冲突,但同时会增加典型性能问题的复杂性
-
-
不同锁的使用限制和注意事项
- 互斥锁:
-
- 由于互斥锁是独占锁,因此在高并发的情况下,可能导致锁竞争过于激烈,导致性能问题。
-
- 在使用互斥锁时,需要确保锁的正确释放,否则会导致死锁的问题。
-
- 递归锁:
-
- 使用递归锁时,需要注意递归次数,否则可能会导致栈溢出的问题。
-
- 在使用递归锁时,需要确保线程在持有锁的同时,还可以执行其他操作,否则可能导致性能问题。
-
- 读写锁:
-
- 当使用读写锁时,需要注意读操作和写操作的相对数量,否则可能导致写操作长时间等待的问题。
-
- 在使用读写锁时,需要注意读操作和写操作的互斥性问题,以避免读写冲突导致的数据不一致。
-
- 自旋锁:
-
- 自旋锁通常适用于锁竞争不激烈的情况下,因为自旋锁在等待锁时会一直占用CPU,会导致CPU资源浪费问题。
-
- 在使用自旋锁时,需要注意自旋次数的设置,否则可能会导致CPU资源浪费的问题。
-
- 互斥锁: