上位:
https://blog.csdn.net/ykun089/article/details/106332805/
https://www.cnblogs.com/TMesh/p/11730847.html
https://blog.csdn.net/ffilman/article/details/4871920
https://www.zhihu.com/question/66733477
文章目录
linux C 读写锁
读写锁是用来解决读者写者问题的,读操作可以共享,写操作是排他的,读可以有多个在读,写只有唯一个在写,同时写的时候不允许读。
对于读数据比修改数据频繁的应用,用读写锁代替互斥锁可以提高效率。
初始化
#include <pthread.h>
int pthread_rwlock_init(pthread_rwlock_t *rwlock, const pthread_rwlockattr_t *attr);
或
pthread_rwlock_t rwlock = PTHREAD_RWLOCK_INITIALIZER;//无对应销毁函数,因为是在静态的,在静态存储区
用PTHREAD_RWLOCK_INITIALIZER 宏初始化,会以静态方式分配读写锁,其作用与通过调用pthread_rwlock_init() 并将参数attr指定为 NULL进行动态初始化等效,区别在于不会执行错误检查。
atrr参数
初始化读写锁属性
int pthread_rwlockattr_init(pthread_rwlockattr_t *attr);
销毁读写锁属性
int pthread_rwlockattr_destroy(pthread_rwlockatttr_t *attr);
设置读写锁属性
int pthread_rwlockattr_setpshared(pthread_rwlockattr_t *attr, int valptr);
获取读写锁属性
int pthread_rwlockattr_getpshared(const pthread_rwlockattr_t *attr, int *valptr);
用来控制是否支持进程之间共享的属性。
#include <pthread.h>
int pthread_rwlockattr_setpshared(pthread_rwlockattr_t *attr, int pshared);
-
PTHREAD_PROCESS_SHARED
允许可访问用于分配读写锁的内存的任何线程对读写锁进行处理。即使该锁是在由多个进程共享的内存中分配的,也允许对其进行处理。 -
PTHREAD_PROCESS_PRIVATE
读写锁只能由某些线程处理,这些线程与初始化该锁的线程在同一进程中创建。如果不同进程的线程尝试对此类读写锁进行处理,则其行为是不确定的。由进程共享的属性的缺省值为 PTHREAD_PROCESS_PRIVATE。 -
PTHREAD_RWLOCK_PREFER_WRITER_NONRECURSIVE_NP
读写锁的写锁饥饿问题: 缺省情况下是reader优先进入lock队列,这会造成writer饿死;
通俗理解:多线程场景,大多数线程是获取读锁,仅有一个线程要获取写锁,而存在写锁基本上抢不到的场景,而产生意想不到的bug。
此时需要设置PTHREAD_RWLOCK_PREFER_WRITER_NONRECURSIVE_NP属性,注意如下也可使用pthread_rwlockattr_setkind_np函数
pthread_rwlockattr_t attr;
pthread_rwlockattr_init(&attr);
pthread_rwlockattr_setkind_np (&attr, PTHREAD_RWLOCK_PREFER_WRITER_NONRECURSIVE_NP);
(void)pthread_rwlock_init(&rwlock, &attr);
#include <pthread.h>
int pthread_rwlockattr_getpshared(const pthread_rwlockattr_t *attr, int *pshared);
获取读锁
#include <pthread.h>
int pthread_rwlock_rdlock(pthread_rwlock_t *rwlock );
获取非阻塞读锁
#include <pthread.h>
int pthread_rwlock_tryrdlock(pthread_rwlock_t *rwlock);
获取写锁
#include <pthread.h>
int pthread_rwlock_wrlock(pthread_rwlock_t *rwlock );
获取非阻塞写锁
#include <pthread.h>
int pthread_rwlock_trywrlock(pthread_rwlock_t *rwlock);
释放锁
#include <pthread.h>
int pthread_rwlock_unlock (pthread_rwlock_t *rwlock);
销毁锁
#include <pthread.h>
int pthread_rwlock_destroy(pthread_rwlock_t *rwlock);
返回值
如果成功,返回0
如果失败,返回错误码
读写锁特点
- 多个读者可以同时进行读
- 写者必须互斥(只允许一个写者写,也不能读者写者同时进行)
- 写者优先于读者(一旦有写者,则后续读者必须等待,唤醒时优先考虑写者)
相关问题
对已初始化的读写锁,再次初始化,则结果是不确定的;
对未初始化的读写锁,进行相关操作,则结果是不确定的;
如果调用者已经获取了写锁,在获取读锁,则结果是不确定的,反之亦然;
对未获取到锁,执行unlock操作,则结果是不确定的;
总结
读写锁默认初始化的场景是用在多线程场景下,并不是多进程场景。
互斥锁
声明定义
pthread_mutex_t lock; /* 互斥锁定义 */
int pthread_mutex_init(pthread_mutex_t *mutex, pthread_mutexattr_t *attr); /* 动态初始化, 成功返回0,失败返回非0 */
或者
pthread_mutex_t thread_mutex = PTHREAD_MUTEX_INITIALIZER; /* 静态初始化 */
获取锁
int pthread_mutex_lock(pthread_mutex_t *mutex); /* 阻塞的锁定互斥锁 */
int pthread_mutex_trylock(pthread_mutex_t *mutex); /* 非阻塞的锁定互斥锁,成功获得互斥锁返回0,如果未能获得互斥锁,立即返回一个错误码 */
#include <pthread.h>
#include <time.h>
int pthread_mutex_timedlock(pthread_mutex_t mutex, const struct timespec *tsptr);//相对于pthread_mutex_lock,此函数不会一直睡眠下去,当入参中的超时时间到达以后,操作系统会唤醒当前线程,如果在超时时间内获得了互斥量,函数返回0,否则非0,另外返回值有一种情况可以表示是超时返回。
释放锁
int pthread_mutex_unlock(pthread_mutex_t *mutex); /* 解锁互斥锁 */
销毁锁
int pthread_mutex_destroy(pthread_mutex_t *mutex) /* 销毁互斥锁 */
返回值
如果成功,返回0
如果失败,返回错误码
mutex已经被锁住的时候无法再获取锁 --- EBUSY
mutex指向的mutex未被初始化 --- EINVAL
lock count(锁数量)已经超过 递归索的最大值,无法再获得该mutex锁 --- EAGAIN
当前线程已经获得该mutex锁 --- EDEADLK
当前线程不是该mutex锁的拥有者 --- EPERM
相关函数说明
初始化互斥锁属性对象 pthread_mutexattr_init 语法
销毁互斥锁属性对象 pthread_mutexattr_destroy 语法
设置互斥锁范围 pthread_mutexattr_setpshared 语法
获取互斥锁范围 pthread_mutexattr_getpshared 语法
设置互斥锁的类型属性 pthread_mutexattr_settype 语法
获取互斥锁的类型属性 pthread_mutexattr_gettype 语法
设置互斥锁属性的协议 pthread_mutexattr_setprotocol 语法
获取互斥锁属性的协议 pthread_mutexattr_getprotocol 语法
设置互斥锁属性的优先级上限 pthread_mutexattr_setprioceiling 语法
获取互斥锁属性的优先级上限 pthread_mutexattr_getprioceiling 语法
设置互斥锁的优先级上限 pthread_mutex_setprioceiling 语法
获取互斥锁的优先级上限 pthread_mutex_getprioceiling 语法
设置互斥锁的强健属性 pthread_mutexattr_setrobust_np 语法
获取互斥锁的强健属性 pthread_mutexattr_getrobust_np 语法
属性设置
相关属性
enum lock_type// 使用pthread_mutexattr_settype来更改
{
PTHREAD_MUTEX_TIMED_NP [default]//当一个线程加锁后,其余请求锁的线程形成等待队列,在解锁后按优先级获得锁。
PTHREAD_MUTEX_ADAPTIVE_NP // 动作最简单的锁类型,解锁后所有线程重新竞争。
PTHREAD_MUTEX_RECURSIVE_NP // 允许同一线程对同一锁成功获得多次。当然也要解锁多次。其余线程在解锁时重新竞争。
PTHREAD_MUTEX_ERRORCHECK_NP // 若同一线程请求同一锁,返回EDEADLK,否则与PTHREAD_MUTEX_TIMED_NP动作相同。 此处特别注意linux和windows下的errno.h中的EDEADLK对应的宏的值有差别:linux下为35,windows下36
} type;
linux下pthread.h中的线程属性, 都是基于如上的定义。
/* Mutex types. */
enum
{
PTHREAD_MUTEX_TIMED_NP,
PTHREAD_MUTEX_RECURSIVE_NP,
PTHREAD_MUTEX_ERRORCHECK_NP,
PTHREAD_MUTEX_ADAPTIVE_NP
#ifdef __USE_UNIX98
,
PTHREAD_MUTEX_NORMAL = PTHREAD_MUTEX_TIMED_NP,
PTHREAD_MUTEX_RECURSIVE = PTHREAD_MUTEX_RECURSIVE_NP,
PTHREAD_MUTEX_ERRORCHECK = PTHREAD_MUTEX_ERRORCHECK_NP,
PTHREAD_MUTEX_DEFAULT = PTHREAD_MUTEX_NORMAL
#endif
#ifdef __USE_GNU
/* For compatibility. */
, PTHREAD_MUTEX_FAST_NP = PTHREAD_MUTEX_TIMED_NP
#endif
};
pthread_mutexattr_t attr;
pthread_mutexattr_init(&attr);// 初始化attr为默认属性
pthread_mutexattr_settype(&attr, PTHREAD_MUTEX_TIMED_NP);// 设置attr属性为PTHREAD_MUTEX_TIMED_NP,即默认属性(当一个线程加锁后,其余请求锁的线程形成等待队列,在解锁后按优先级获得锁)
条件变量
专门是与互斥锁搭配使用的
以生产者消费者模型为例: 生产者 ----> 消息队列 ----> 消费者,生产者和消费者正常情况下不会出现在同一个线程中,这样的话,他们对于消息队列的访问就涉及 “竞争”。
如果使用Mutex实现,那么“生产者在插入队列之前需要先加锁,操作完成再解锁”,而消费者 “同样需要先加锁队列,然后从队列中读数据,结束后再解锁队列”。这样的逻辑是OK的,但是在生产者效率明显低于消费者的情况下,这个模型不是好模型,因为生产者的产出能力不强,那么大量的cpu都消耗在消费者的加锁解锁上了,大部分情况下消费者都是无法从队列中读到数据的。
如果使用条件变量实现,就可以很好解决这个问题,区别在于,消费者在第一次加锁pthread_mutex_lock后,会使用pthread_cond_wait释放刚才加的锁,同时自己进入睡眠状态,而投入睡眠的动作同时会把自己唤醒的条件一并告诉内核,当内核发现条件满足,则消费者线程从pthread_cond_wait函数返回。
初始化
#include <pthread.h>
int pthread_cond_init(pthread_cond_t *cv, const pthread_condattr_t *cattr);
//或者
pthread_cond_t cv = PTHREAD_COND_INITIALIZER;
当参数cattr为空指针时,函数创建的是一个缺省的条件变量。
阻塞在条件变量上
#include <pthread.h>
int pthread_cond_wait(pthread_cond_t *cv, pthread_mutex_t *mutex);
等待一个条件变量,入参需要指定一个“已锁的”mutex,这个mutex在pthread_cond_wait 内部会被解锁,然后内核会用入参的条件变量对象来锁住当前线程。
阻塞直到指定时间
#include <pthread.h>
#include <time.h>
int pthread_cond_timedwait(pthread_cond_t *cv, pthread_mutex_t *mp, const structtimespec * abstime);//超时等待一个条件变量,如果超时,此函数会返回,再次调用时,请保证传入的mutex是加了锁的,这点容易遗漏
函数到了一定的时间,即使条件未发生也会解除阻塞。这个时间由参数abstime指定。
解除在条件变量上的阻塞
#include <pthread.h>
int pthread_cond_signal(pthread_cond_t *cv);//随机发送给一个pthread_cond_wait 等待的线程
函数被用来释放被阻塞在指定条件变量上的一个线程。
注意:必须在互斥锁的保护下使用相应的条件变量。否则对条件变量的解锁有可能发生在锁定条件变量之前,从而造成死锁。
释放阻塞的所有线程
#include <pthread.h>
int pthread_cond_broadcast(pthread_cond_t *cv);
函数唤醒所有阻塞在某个条件变量上的线程, 这些线程被唤醒后将再次竞争相应的互斥锁
释放条件变量
#include <pthread.h>
int pthread_cond_destroy(pthread_cond_t *cv);
注意:条件变量占用的空间并未被释放。
返回值
如果成功,返回0
如果失败,返回错误码
唤醒丢失问题
唤醒丢失往往会在下面的情况下发生:
1、一个线程调用pthread_cond_signal或pthread_cond_broadcast函数;
2、另一个线程正处在测试条件变量和调用pthread_cond_wait函数之间;
生产者和消费者示例
#include <stdio.h>
#include <pthread.h>
#define MAX 5
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
pthread_cond_t notfull = PTHREAD_COND_INITIALIZER; //是否队满
pthread_cond_t notempty = PTHREAD_COND_INITIALIZER; //是否队空
int top = 0;
int bottom = 0;
void* produce(void* arg)
{
int i;
for ( i = 0; i < MAX*2; i++)
{
pthread_mutex_lock(&mutex);
while ((top+1)%MAX == bottom)
{
printf("full! producer is waiting\n");
//等待队不满
pthread_cond_wait(notfull, &mutex);
}
top = (top+1) % MAX;
//发出队非空的消息
pthread_cond_signal(notempty);
pthread_mutex_unlock(&mutex);
}
return (void*)1;
}
void* consume(void* arg)
{
int i;
for ( i = 0; i < MAX*2; i++)
{
pthread_mutex_lock(&mutex);
while ( top%MAX == bottom)
{
printf("empty! consumer is waiting\n");
//等待队不空
pthread_cond_wait(notempty, &mutex);
}
bottom = (bottom+1) % MAX;
//发出队不满的消息
pthread_cond_signal(notfull);
pthread_mutex_unlock(&mutex);
}
return (void*)2;
}
int main(int argc, char *argv[])
{
pthread_t thid1;
pthread_t thid2;
pthread_t thid3;
pthread_t thid4;
int ret1;
int ret2;
int ret3;
int ret4;
pthread_create(&thid1, NULL, produce, NULL);
pthread_create(&thid2, NULL, consume, NULL);
pthread_create(&thid3, NULL, produce, NULL);
pthread_create(&thid4, NULL, consume, NULL);
pthread_join(thid1, (void**)&ret1);
pthread_join(thid2, (void**)&ret2);
pthread_join(thid3, (void**)&ret3);
pthread_join(thid4, (void**)&ret4);
return 0;
}
自旋锁
// 声明一个自旋锁变量
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类型)。表示的是是否能进程间共享自旋锁
- PTHREAD_PROCESS_PRIVATE:仅同进程下读线程可以使用该自旋锁
- PTHREAD_PROCESS_SHARED:不同进程下的线程可以使用该自旋锁
锁
自旋锁spinlock
主要特征:获取不到锁的时候,会直接等待,不会被CPU直接调度走,而是会一直check直到获取到锁,因为是一直等待,所以不会有调度的开销。
优点:避免了系统的唤醒,自己来执行轮询,如果临界区中代码非常短且是原子的,那么使用起来是非常方便的,避免了各种上下文切换,开销非常小,因此在内核的一些数据结构中自旋锁被广泛的使用。
缺点:
因不停的查看锁的释放情况,故会浪费更多的CPU资源
互斥锁mutex
互斥锁无法获取锁时,将阻塞睡眠,需要系统来唤醒,互斥锁由系统来唤醒。
使用场景: 多个使用者获取锁,都在阻塞睡眠,当锁释放时,多个使用者都被系统唤醒,但是此时仅会有一个使用者获取锁成功,其他使用者还是继续阻塞睡眠,这些线程可理解为被虚假唤醒。(至于谁会获取锁成功,这个机制不在此讨论,无论是有队列保障顺序,还是各个使用者进行抢占式,都是系统层来进行保障的,使用者不用担心。)
关于阻塞睡眠的理解:
跟上面的自旋锁比较,可理解为,如果用锁时已经被锁,则放弃cpu执行权,至于什么时候再次获取CPU的执行权,则等待系统的通知。
读写锁rwlock
读写锁也叫共享互斥锁:读模式共享和写模式互斥。属于互斥锁的进化。
在写加锁模式下,其他使用者获取读锁会被阻塞睡眠,直到锁被释放。
RCU锁
RCU锁是读写锁的扩展版本,简单来说就是支持多读多写同时加锁,多读没什么好说的,但是对于多写同时加锁,还是存在一些技术挑战的。
RCU锁翻译为Read Copy Update Lock,读-拷贝-更新 锁。
- Copy拷贝:写者在访问临界区时,写者将先拷贝一个临界区副本,然后对副本进行修改;
- Update更新:RCU机制将在在适当时机使用一个回调函数把指向原来临界区的指针重新指向新的被修改的临界区,锁机制中的垃圾收集器负责回调函数的调用。
- 更新时机:没有CPU再去操作这段被RCU保护的临界区后,这段临界区即可回收了,此时回调函数即被调用。
从实现逻辑来看,RCU锁在多个写者之间的同步开销还是比较大的,涉及到多份数据拷贝,回调函数等,
因此这种锁机制的使用范围比较窄,适用于读多写少的情况,如网络路由表的查询更新、设备状态表更新等,在业务开发中使用不是很多。
可重入锁和不可重入锁
- 递归锁recursive mutex 可重入锁(reentrant mutex)
- 非递归锁non-recursive mutex 不可重入锁(non-reentrant mutex)
Windows下的Mutex和Critical Section是可递归的;
Linux下的pthread_mutex_t锁默认是非递归的。
在Linux中可以显式设置PTHREAD_MUTEX_RECURSIVE属性,将pthread_mutex_t设为递归锁避免这种场景。 同一个线程可以多次获取同一个递归锁,不会产生死锁。而如果一个线程多次获取同一个非递归锁,则会产生死锁。
如下代码对于非递归锁的死锁示例:
MutexLock mutex;
void testa()
{
mutex.lock();
do_sth();
mutex.unlock();
}
void testb()
{
mutex.lock();
testa();
mutex.unlock();
}
c++相关
C++11开始引入了多线程库<thread>,其中也包含了互斥锁的API:std::muxtex 。
C++11中有递归互斥量的API:std::recursive_mutex。
C++11中也有条件变量的API: std::condition_variable。
C++11中有互斥量、条件变量但是并没有引入读写锁。而在C++17中出现了一种新锁:std::shared_mutex。