互斥锁,条件锁,读写锁,自旋锁

本文介绍了多线程编程中的几种同步机制,包括互斥量(mutex)、条件变量和读写锁。互斥量用于保护临界区,防止多个线程同时访问,有非阻塞的trylock函数。条件变量提供线程间通信,常与互斥量配合使用。读写锁允许多线程并发读取,独占写入。自旋锁则是一种忙等待的锁机制,不涉及线程休眠。选择哪种锁取决于具体场景和性能需求。
摘要由CSDN通过智能技术生成

mutex(互斥量)
mutux即互斥量,也就是常说的互斥锁,尽管名称不含lock,但是称之为锁,也是没有太大问题的。mutes无疑是最常见的多线程同步方式,其思想简单粗暴,多线程共享一个互斥量,然后线程之间去竞争。得到锁的线程可以进入临界区执行代码

// 声明一个互斥量    
pthread_mutex_t mtx;

// 初始化 
pthread_mutex_init(&mtx, NULL);

// 加锁 
pthread_mutex_lock(&mtx);

// 解锁 
pthread_mutex_unlock(&mtx);

// 销毁
pthread_mutex_destroy(&mtx); 

mutex是睡眠等待类型的锁,当线程抢互斥锁资源失败的时候,线程会陷入休眠,优点是节省CPU资源,缺点是休眠唤醒会消耗一点时间,自从Linux 2.6版以后,mutex完全用futex的API实现了,内部系统调用的开销大大减小。值得一提的是,pthread的锁一般都有一个trylock的函数。比如对于互斥量

ret = pthread_mutex_trylock(&mtx);
if (0 == ret) { // 加锁成功
    ... 
    pthread_mutex_unlock(&mtx);
} else if(EBUSY == ret){ // 锁正在被使用;
    ... 
}

pthread_mutex_trylock用于以非阻塞的模式来请求互斥量,就好比各种IO函数都有一个nonblock的模式一样,加锁这件事也有类似的非阻塞模式

当线程尝试加锁时,如果锁已经被其他线程锁定,该线程就会被阻塞住,直到能成功acquire。但有时候我们不希望这样。pthread_mutex_trylock在被其他线程锁定时,会返回特殊错误码,加锁成返回0,仅当成功的时候,我们才能解锁在后面进行解锁操作

C++11开始引入多线程库,其中也包含了互斥锁的API:std::mutex
此外,依据同一线程是否能多次加锁,把互斥量又分为以下两类:
是:成为递归互斥量,也称可重入锁
否:即非递归互斥量,也称不可重入锁
若同一线程对非递归的互斥量多次加锁,可能会造成死锁。递归互斥量则无此风险。C++11中有递归互斥量的API:std::recursive_mutex。对于pthread则可以通过mutex添加PTHREAD_MUTEX_RECURSIVE属性的方式来使用递归互斥量:

// 声明一个互斥量 
pthread_mutex_t mtx; 
// 声明一个互斥量的属性变量 
pthread_mutexattr_t mtx_attr;  
// 初始化互斥量的属性变量 
pthread_mutexattr_init(&mtx_attr); 
// 设置递归互斥量的属性 
pthread_mutexattr_settype(&mtx_attr, PTHREAD_MUTEX_RECURSIVE);  
// 把属性赋值给互斥量 
pthread_mutext_init(&mtx, &mutext_attr);  

然而对于递归互斥量可重入锁的使用则需要克制

condition variable(条件变量)
请注意条件变量不是锁,它是一种线程间的通信机制,并且几乎总是和互斥量一起使用的。所以互斥量和条件变量两者一般都是成套出现的。比如C++中也有条件变量的API std::condition_variable
对于pthread:

// 声明一个互斥量      
pthread_mutex_t mtx; 
// 声明一个条件变量 
pthread_cond_t cond; 
...  
// 初始化  
pthread_mutex_init(&mtx, NULL); 
pthread_cond_init(&cond, NULL);  
// 加锁   
pthread_mutex_lock(&mtx); 
// 加锁成功,等待条件变量触发 
pthread_cond_wait(&cond, &mtx);  
... 
// 加锁   
pthread_mutex_lock(&mtx); 
pthread_cond_signal(&cond); 
...  
// 解锁  
pthread_mutex_unlock(&mtx); 
// 销毁 
pthread_mutex_destroy(&mtx); 

pthread_cond_wait函数会把条件变量和互斥量都传入,并且多线程调用的时候条件变量和互斥量一定要一一对应,不能一个条件变量在不同线程中wait的时候传入不同的互斥量,否则是未定义结果

关于是先解锁互斥量还是先进行条件变量的通知,是一个比较大的议题,有种论断说:先解锁互斥量再通知条件变量可以减少多余的上下文切换,从而提高效率。这种说法是基于一种实现假设:先通知条件变量,再解锁。可能让其他等待条件变量的线程被唤醒了,但是此时互斥量还没解锁,从而再次陷入休眠,然而对于另外一些实现,比如Linux系统,则通过等待变形解决了这一问题,所以先通知再解锁也没有问题

另外在使用条件变量的过程中有个稍微违反直觉的写发,那就是使用while做判断状态是否满足而不是if,这样做的原因有二
1.避免惊群
2.避免某些情况下线程被虚假唤醒

read-write lock(读写锁)
顾名思义[读写锁]就是对于临界区区分读和写,再读多写少的场景下,不加区分的使用互斥量显然是有点浪费的。此时便该上演读写锁的拿手好戏

读写锁有一个别称叫[共享-独占锁]。不过单看[共享-独占锁]或者[读写锁]这两个名称,其实并未区分对于读和写,到底谁共享,谁独占,可能会让人误以为读写锁时一种更为泛化的称呼,其实不是。读写锁的含义是准确的:是一种“读共享,写独占”的锁

读写锁的特性:
1.当读写锁被加了写锁时,其他线程对该锁加读锁或者写锁都会阻塞(不是失败)
2.当读写锁倍加了读锁时,其他线程对该锁加写锁会阻塞,加读锁会成功
因而适用于多读少写的场景

// 声明一个读写锁 
pthread_rwlock_t rwlock; 
... 
// 在读之前加读锁 
pthread_rwlock_rdlock(&rwlock);  
... 共享资源的读操作  
// 读完释放锁 
pthread_rwlock_unlock(&rwlock);  
// 在写之前加写锁 
pthread_rwlock_wrlock(&rwlock);   
... 共享资源的写操作  
// 写完释放锁 
pthread_rwlock_unlock(&rwlock);  
// 销毁读写锁 
pthread_rwlock_destroy(&rwlock); 

其实加读锁和加写锁这两个说法可能会造成误导,让人误以为是由两把锁,其实读写锁是一个锁。所谓加读锁和加写锁,准确的说法可能是[给读写锁加读模式的锁定和加写模式的锁定]

读写锁和互斥量一样也有trylock函数,也是以非阻塞的形式来请求锁,不会导致阻塞

另外多读少些的场景有些特殊场景,可以用特殊的数据结构减少锁使用
1.多读但写的线性结构。用数组实现环形队列,避免vector等动态扩张的数据结构,写在结尾,由于单写因而可以不加锁;读在开头,由于多读(避免重复消费)所以需要加一下锁(互斥量就行)
2.多读单写的KV,可以使用双缓冲的数据结构来实现。double buffer同名的概念比较多,这里指的是forground和backgroud两个buffer进行切换的[0-1切换]技术。比如实现动态加载配置文件的时候,可能会再切换间隙加一个短暂的互斥量,但是基本可以认为是lock free的

spinlock(自旋锁)
自旋之名颇为玄妙,第一次听闻常让人略觉高大。但和无数个好似『故意把简单概念复杂化』的计算机术语一样,自旋锁的本质简单的难以置信。

要了解自旋锁,首先了解自旋。什么是自旋(spin)呢?更为通俗的一个词是『忙等待』(busy waiting)。最最通俗的一个理解,其实就是死循环……。

单看使用方法和使用互斥量的代码是差不多的。只不过自旋锁不会引起线程休眠。当共享资源的状态不满足的时候,自旋锁会不停地循环检测状态。因为不会陷入休眠,而是忙等待的方式也就不需要条件变量。

这是优点也是缺点。不休眠就不会引起上下文切换,但是会比较浪费CPU。

// 声明一个自旋锁变量 
pthread_spinlock_t spinlock;  
// 初始化    
pthread_spin_init(&spinlock, 0);  
// 加锁   
pthread_spin_lock(&spinlock);  
// 解锁  
pthread_spin_unlock(&spinlock);  
// 销毁   
pthread_spin_destroy(&spinlock);

pthread_spin_init函数的第二个参数名为pshared(int类型)。表示的是是否能进程间共享自旋锁。这被称之为Thread Process-Shared Synchronization。互斥量的通过属性也可以把互斥量设置成进程间共享的。pshared有两个枚举值:

PTHREAD_PROCESS_PRIVATE:仅同进程下读线程可以使用该自旋锁
PTHREAD_PROCESS_SHARED:不同进程下的线程可以使用该自旋锁
在Linux上的glibc中这两个枚举值分别是0和1(Mac上不是)。所以通常也会看到直接传0的代码。你可能觉得不使用宏,直接用数字硬编码不是一个好习惯。的确,妥妥的Magic Number,但还有一个有趣的事实你需要了解:并不是所有实现都支持自旋锁设置两种pshared。比如:

int pthread_spin_init (pthread_spinlock_t *lock, int pshared) {     
    /* Relaxed MO is fine because this is an initializing store.  */     	atomic_store_relaxed (lock, 0);     
   return 0; 
} 

所以直接传0可能也无伤大雅。

自旋锁 VS 互斥量+条件变量 孰优孰劣?肯定要看具体的使用场景,当你不知道在你的使用场景下这两种锁该用哪个的时候,那就是用互斥量吧!或者通过压测的判断,不过大多数时候我们好像并不需要这么一个pthread的自旋锁

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值