目录
互斥体
互斥锁
属性
使用流程(相关API函数)
互斥锁初始化和销毁的函数原型:
互斥锁的上锁和解锁的函数原型为:
pthread_mutex_init()
定义
函数原型
返回值
初始化
pthread_mutex_destroy()
定义
函数原型
pthread_mutex_lock()(阻塞)
定义
函数原型
返回值
基于mutex类型的返回值
pthread_mutex_unlock()
定义
函数原型
返回值
互斥锁应用举例
不使用互斥锁
使用互斥锁
自旋锁
定义
适用场景
特性
自旋锁的死锁
使用流程(相关API)
pthread_spin_init()
作用
函数原型
参数
返回值
pthread_spin_lock()
作用
函数原型
返回值
pthread_spin_unlock()
函数原型
返回值
pthread_spin_destroy()
作用
函数原型
返回值
自旋锁与互斥锁的区别
测试样例
互斥锁
结果分析
读写锁
读者写者问题
定义
三种状态
特性
使用流程(相关API)
pthread_rwlock_init()
作用
函数原型
参数
返回值
pthread_rwlock_rdlock() (阻塞)
作用
函数原型
返回值
注意事项
pthread_rwlock_tryrdlock()(非阻塞)
作用
函数原型
返回值
注意事项
pthread_rwlock_wrlock() (非阻塞)
作用
函数原型
返回值
注意事项
pthread_rwlock_trywrlock()(非阻塞)
作用
函数原型
返回值
注意事项
pthread_rwlock_unlock()
作用
函数原型
返回值
注意事项
pthread_rwlock_destroy()
作用
函数原型
返回值
注意事项
测试代码
互斥锁、读写锁、自旋锁区别
读写锁的三种实现
使用互斥锁和条件变量实现读写锁
使用2个互斥锁实现读写锁
用mutex和conditon实现写优先的读写锁
互斥体
- 互斥体实现了“互相排斥”(mutual exclusion)同步的简单形式(所以名为互斥体(mutex))。互斥体禁止多个线程同时进入受保护的代码“临界区”(critical section)。因此,在任意时刻,只有一个线程被允许进入这样的代码保护区。
- 任何线程在进入临界区之前,必须获取(acquire)与此区域相关联的互斥体的所有权。如果已有另一线程拥有了临界区的互斥体,其他线程就不能再进入其中。这些线程必须等待,直到当前的属主线程释放(release)该互斥体。
- 什么时候需要使用互斥体呢?互斥体用于保护共享的易变代码,也就是,全局或静态数据。这样的数据必须通过互斥体进行保护,以防止它们在多个线程同时访问时损坏
互斥锁
- 为了保证共享数据操作的完整性。每个对象都对应于一个可称为" 互斥锁" 的标记,这个标记用来保证在任一时刻,只能有一个线程访问该对象。
- 使用互斥锁(互斥)可以使线程按顺序执行。通常,互斥锁通过确保一次只有一个线程执行代码的临界段来同步多个线程。互斥锁还可以保护单线程代码
一段需要互斥执行的代码称为临界区。线程在进入临界区之前,首先尝试加锁lock(),如果成功,则进入临界区,此时称这个线程持有锁;否则,就等待,直到持有锁的线程解锁;持有锁的线程执行完临界区的代码后,执行解锁unlock()。
属性
- 原子性:把一个互斥锁定义为一个原子操作,这意味着操作系统保证了如果一个线程锁定了互斥锁,则没有其他线程可以在同一时间成功锁定这个互斥量。
- 唯一性:如果一个线程锁定一个互斥量,在它接触锁定之前,没有其他线程可以锁定这个互斥量。
- 非繁忙等待:如果一个线程已经锁定了一个互斥锁,第二个线程又试图去锁定这个互斥锁,则第二个线程将被挂起(不占用CPU资源),直到第一个线程解锁,第二个线程则被唤醒并继续执行,同时锁定这个互斥量
使用流程(相关API函数)
互斥锁初始化和销毁的函数原型:
int pthread_mutex_init(pthread_mutex_t *restrict mutex,const pthread_mutexattr_t *restrict attr);
int pthread_mutex_destroy(pthread_mutex_t *mutex);
互斥锁的上锁和解锁的函数原型为:
int pthread_mutex_lock(pthread_mutex_t *mutex);
int pthread_mutex_trylock(pthread_mutex_t *mutex);
int pthread_mutex_unlock(pthread_mutex_t *mutex);
pthread_mutex_init()
#include <pthread.h>//头文件
定义
互斥锁的初始化
函数原型
int pthread_mutex_init(pthread_mutex_t *restrict mutex,const pthread_mutexattr_t *restrict attr);
pthread_mutex_t: 定义互斥体机制完成多线程的互斥操作,该机制的作用是对某个需要互斥的部分,在进入时先得到互斥体,如果没有得到互斥体,表明互斥部分被其它线程拥有,此时欲获取互斥体的线程阻塞,直到拥有该互斥体的线程完成互斥部分的操作为止。
mutex:是指向要初始化的互斥锁的指针。
attr: 是指向属性对象的指针,该属性对象定义要初始化的互斥锁的属性。如果该指针为 NULL,则使用默认的属性。,默认属性为快速互斥锁
- PTHREAD_MUTEX_TIMED_NP,这是缺省值,也就是普通锁。当一个线程加锁以后,其余请求锁的线程将形成一个等待队列,并在解锁后按优先级获得锁。这种锁策略保证了资源分配的公平性。
- PTHREAD_MUTEX_RECURSIVE_NP,嵌套锁,允许同一个线程对同一个锁成功获得多次,并通过多次unlock解锁。如果是不同线程请求,则在加锁线程解锁时重新竞争。
- PTHREAD_MUTEX_ERRORCHECK_NP,检错锁,如果同一个线程请求同一个锁,则返回EDEADLK,否则与PTHREAD_MUTEX_TIMED_NP类型动作相同。这样保证当不允许多次加锁时不出现最简单情况下的死锁。
- PTHREAD_MUTEX_ADAPTIVE_NP,适应锁,动作最简单的锁类型,仅等待解锁后重新竞争。
返回值
成功则返回0, 出错则返回错误编号。
初始化
- 静态初始化:如果互斥锁 mutex 是静态分配的(定义在全局,或加了static关键字修饰),可以直接使用宏进行初始化。
pthead_mutex_t muetx = PTHREAD_MUTEX_INITIALIZER;
- 动态初始化:局部变量应采用动态初始化。
pthread_mutex_init(&mutex, NULL)
pthread_mutex_destroy()
定义
mutex 指向要销毁的互斥锁的指针
函数原型
int pthread_mutex_destroy(pthread_mutex_t *mutex);
互斥锁销毁函数在执行成功后返回 0,否则返回错误码。
pthread_mutex_lock()(阻塞)
定义
对共享资源的访问, 要对互斥量进行加锁, 如果互斥量已经上了锁, 调用线程会阻塞, 直到互斥量被解锁. 在完成了对共享资源的访问后, 要对互斥量进行解锁。
函数原型
int pthread_mutex_lock(pthread_mutex_t *mutex);
返回值
成功则返回0, 出错则返回错误编号.
int pthread_mutex_trylock(pthread_mutex_t *mutex);
说明: 具体说一下trylock函数, 这个函数是非阻塞调用模式, 也就是说, 如果互斥量没被锁住, trylock函数将把互斥量加锁, 并获得对共享资源的访问权限; 如果互斥量被锁住了, trylock函数将不会阻塞等待而直接返回EBUSY, 表示共享资源处于忙状态。
基于mutex类型的返回值
- 如果mutex 对象的type是 PTHREAD_MUTEX_NORMAL,不进行deadlock detection(死锁检测)。企图进行relock,这个mutex会导致deadlock.如果一个线程对未加锁的或已经unlock的mutex对象进行unlock操作,结果是不未知的。
- 如果mutex类型是PTHREAD_MUTEX_ERRORCHECK,那么将进行错误检查。如果一个线程企图对一个已经锁住的mutex进行relock,将返回一个错误。如果一个线程对未加锁的或已经unlock的mutex对象进行unlock操作,将返回一个错误。
- 如果mutex类型是 PTHREAD_MUTEX_RECURSIVE,mutex会有一个锁住次数(lock count)的概念。当一个线程成功地第一次锁住一个mutex的时候,锁住次数(lock count)被设置为1,每一次一个线程unlock这个mutex的时候,锁住次数(lock count)就减1。当锁住次数(lock count)减少为0的时候,其他线程就能获得该mutex锁了。如果一个线程对未加锁的或已经unlock的mutex对象进行unlock操作,将返回一个错误。
- 如果mutex类型是 PTHREAD_MUTEX_DEFAULT,企图递归的获取这个mutex的锁的结果是不确定的。unlock一个不是被调用线程锁住的mutex的结果也是不确定的。企图unlock一个未被锁住的mutex导致不确定的结果。
pthread_mutex_unlock()
定义
函数释放有参数mutex指定的mutex对象的锁
函数原型
int pthread_mutex_unlock(pthread_mutex_t *mutex);
返回值
成功则返回0, 出错则返回错误编号.
互斥锁应用举例
由于共享、竞争而没有加任何同步机制,导致产生于时间有关的错误,造成数据混乱:
不使用互斥锁
#include <stdio.h>
#include <string.h>
#include <pthread.h>
#include <stdlib.h>
#include <unistd.h>
void *tfn(void *arg)
{
for(int i =0;i < 5;i++)
{
printf("hi, ");
sleep(1);
printf(" boy \n");
sleep(1);
}
return NULL;
}
int main(void)
{
pthread_t tid;
pthread_create(&tid, NULL, tfn, NULL);
for(int i =0;i < 5;i++)
{
printf("hello, ");
sleep(1);
printf(" girl\n");
sleep(1);
}
pthread_join(tid , NULL);
return 0;
}
输出结果是无序的
使用互斥锁
#include <stdio.h>
#include <string.h>
#include <pthread.h>
#include <stdlib.h>
#include <unistd.h>
pthread_mutex_t mutex;
void *tfn(void *arg)
{
for(int i =0;i < 5;i++)
{
pthread_mutex_lock(&mutex);
printf("hi, ");
sleep(1);
printf(" boy \n");
pthread_mutex_unlock(&mutex);
sleep(1);
}
return NULL;
}
int main(void)
{
pthread_t tid;
pthread_mutex_init(&mutex, NULL);
pthread_create(&tid, NULL, tfn, NULL);
for(int i =0;i < 5;i++)
{
printf("hello, ");
sleep(1);
printf(" girl\n");
sleep(1);
}
pthread_mutex_destroy(&mutex);
pthread_join(tid , NULL);
return 0;
}
多线程实验
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <pthread.h>
pthread_mutex_t mutex;
int val = 0;
void *ThreadFunc(void *arg)
{
while(1)
{
pthread_mutex_lock(&mutex); /*获取互斥锁*/
int i = 0;
for(i = 0 ; i < 10000 ; i++)
{
val++;
}
printf("val = %d\n" , val);
pthread_mutex_unlock(&mutex); /*释放互斥锁*/
usleep(20);
}
return 0;
}
int main()
{
void *ResVal;
pthread_mutex_init (&mutex, NULL); /*定义*/
pthread_t thread1 , thread2;
pthread_create(&thread1 , NULL , ThreadFunc , NULL);
pthread_create(&thread2 , NULL , ThreadFunc , NULL);
pthread_join(thread1 , &ResVal);
pthread_join(thread2 , &ResVal);
}
自旋锁
定义
- 自旋锁它是为为实现保护共享资源而提出一种锁机制。其实,自旋锁与互斥锁比较类似,它们都是为了解决对某项资源的互斥使用。无论是互斥锁,还是自旋锁,在任何时刻,最多只能有一个保持者,也就说,在任何时刻最多只能有一个执行单元获得锁。
- 两者在调度机制上略有不同。对于互斥锁,如果资源已经被占用,资源申请者只能进入睡眠状态。但是自旋锁不会引起调用者睡眠,如果自旋锁已经被别的执行单元保持,调用者就一直循环在那里看是否该自旋锁的保持者已经释放了锁(忙循环)
适用场景
- 自旋锁比较适用于锁使用者保持锁时间比较短的情况。
正是由于自旋锁使用者一般保持锁时间非常短,自旋锁的效率远高于互斥锁。
- 信号量和读写信号量适合于保持时间较长的情况,它们会导致调用者睡眠,因此只能在进程上下文使用,而自旋锁适合于保持时间非常短的情况,它可以在任何上下文使用。如果被保护的共享资源只在进程上下文访问,使用信号量保护该共享资源非常合适,如果对共享资源的访问时间非常短,自旋锁也可以。
自旋锁保持期间是抢占失效的,而信号量和读写信号量保持期间是可以被抢占的。
- 单CPU非抢占内核下:自旋锁会在编译时被忽略(因为单CPU且非抢占模式情况下,不可能发生进程切换,时钟只有一个进程处于临界区(自旋锁实际没什么用了)
- 单CPU抢占内核下:自选锁仅仅当作一个设置抢占的开关(因为单CPU不可能有并发访问临界区的情况,禁止抢占就可以保证临街区唯一被拥有)
- 多CPU下:此时才能完全发挥自旋锁的作用,自旋锁在内核中主要用来防止多处理器中并发访问临界区,防止内核抢占造成的竞争。
- 操作是原子的,因为当多个线程在给定时间自旋,也只有一个线程可以获得该锁。
特性
- 被自旋锁保护的临界区代码执行时不能进入休眠。
- 被自旋锁保护的临界区代码执行时是不能被被其他中断中断。
- 被自旋锁保护的临界区代码执行时,内核不能被抢占。
- 在自旋锁忙等期间,因为并没有进入临界区,所以内核抢占还是有效的,因此,等待自旋锁释放的进程有可能被更高优先级的所取代
- 存在两个问题:死锁和过多占用cpu资源。
- 从这几个特性可以归纳出一个共性:被自旋锁保护的临界区代码执行时,它不能因为任何原因放弃处理器。
自旋锁的死锁
- 内核代码请求到一个自旋锁并且在它的临界区里做它的事情,在中间某处,你的代码失去了处理器。或许它已调用了一个函数(copy_from_user,假设)使进程进入睡眠。也或许,内核抢占发威,一个更高优先级的进程将你的代码推到了一边。此时,正好某个别的线程想获 取同一个锁,如果这个线程运行在和你的内核代码不同的处理器上(幸运的情况),那么它可能要自旋等待一段时间(可能很长),当你的代码从休眠中唤醒或者重新得到处理器并释放锁,它就能得到锁。而最坏的情况是,那个想获取锁得线程刚好和你的代码运行在同一个处理器上,这时它将一直持有CPU进行自旋操作,而你的代码是永远不可能有任何机会来获得CPU释放这个锁了,这就是悲催的死锁。
- 假设我们的驱动程序正在运行,并且已经获取了一个自旋锁,这个锁控制着对设备的访问。在拥有这个锁得时候,设备产生了一个中断,它导致中断处理例程被调用,而中断处理例程在访问设备之前,也要获得这个锁。当中断处理例程和我们的驱动程序代码在同一个处理器上运行时,由于中断处理例程持有CPU不断自旋,我们的代码将得不到机会释放锁,这也将导致死锁。
使用流程(相关API)
#include <pthread.h>
pthread_spin_init()
作用
初始化spin lock, 当线程使用该函数初始化一个未初始化或者被destroy过的spin lock有效。该函数会为spin lock申请资源并且初始化spin lock为unlocked状态
函数原型
int pthread_spin_init(pthread_spinlock_t *lock, int pshared);
参数
- pshared取值:
- PTHREAD_PROCESS_SHARED:该自旋锁可以在多个进程中的线程之间共享。(可以被其他进程中的线程看到)
- PTHREAD_PROCESS_PRIVATE:仅初始化本自旋锁的线程所在的进程内的线程才能够使用该自旋锁。
返回值
若成功,返回0;否则,返回错误编号
pthread_spin_lock()
作用
用来获取(锁定)指定的自旋锁. 如果该自旋锁当前没有被其它线程所持有,则调用该函数的线程获得该自旋锁.否则该函数在获得自旋锁之前不会返回。
函数原型
int pthread_spin_lock(pthread_spinlock_t *lock);
返回值
若成功,返回0;否则,返回错误编号。
EBUSY A thread currently holds the lock.These functions shall not return an error code of [EINTR].如果调用该函数的线程在调用该函数时已经持有了该自旋锁,则结果是不确定的。
pthread_spin_unlock()
函数原型
int pthread_spin_unlock(pthread_spinlock_t *lock);
返回值
若成功,返回0;否则,返回错误编号
pthread_spin_destroy()
作用
用来销毁指定的自旋锁并释放所有相关联的资源(所谓的所有指的是由pthread_spin_init自动申请的资源)
函数原型
int pthread_spin_destroy(pthread_spinlock_t *lock);
返回值
若成功,返回0;否则,返回错误编号
- 在调用该函数之后如果没有调用pthread_spin_init重新初始化自旋锁,则任何尝试使用该锁的调用的结果都是未定义的。
- 如果调用该函数时自旋锁正在被使用或者自旋锁未被初始化则结果是未定义的。
自旋锁与互斥锁的区别
Mutex(互斥锁):
- sleep-waiting类型的锁
- 与自旋锁相比它需要消耗大量的系统资源来建立锁;随后当线程被阻塞时,线程的调度状态被修改,并且线程被加入等待线程队列;最后当锁可用时,在获取锁之前,线程会被从等待队列取出并更改其调度状态;但是在线程被阻塞期间,它不消耗CPU资源。
- 互斥锁适用于那些可能会阻塞很长时间的场景。
- 临界区有IO操作
- 临界区代码复杂或者循环量大
- 临界区竞争非常激烈
- 单核处理器
Spin lock(自旋锁)
- busy-waiting类型的锁
- 对于自旋锁来说,它只需要消耗很少的资源来建立锁;随后当线程被阻塞时,它就会一直重复检查看锁是否可用了,也就是说当自旋锁处于等待状态时它会一直消耗CPU时间。
- 自旋锁适用于那些仅需要阻塞很短时间的场景
测试样例
互斥锁
#include <iostream>
#include <thread>
#include <pthread.h>
#include <sys/time.h>
#include <unistd.h>
int num = 0;
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
int64_t get_current_timestamp()
{
struct timeval now = {0, 0}; //定义时间结构体,前面参数是秒,后面是微妙
gettimeofday(&now, NULL);//获取当前时间
return now.tv_sec * 1000 * 1000 + now.tv_usec;
}
void thread_proc()
{
for(int i=0; i<1000000; ++i){
pthread_mutex_lock(&mutex);
++num;
pthread_mutex_unlock(&mutex);
}
}
int main()
{
int64_t start = get_current_timestamp();
std::thread t1(thread_proc), t2(thread_proc);//C++11定义线程
t1.join();
t2.join();
std::cout<<"num:"<<num<<std::endl;
int64_t end = get_current_timestamp();
std::cout<<"cost:"<<end-start<< " us" << std::endl;
pthread_mutex_destroy(&mutex); //maybe you always foget this
return 0;
}
读写锁
#include <iostream>
#include <thread>
#include <pthread.h>
#include <sys/time.h>
#include <unistd.h>
int num = 0;
pthread_spinlock_t spin_lock;
int64_t get_current_timestamp()
{
struct timeval now = {0, 0};//定义时间结构体,前面参数是秒,后面是微妙
gettimeofday(&now, NULL);//获取当前时间
return now.tv_sec * 1000 * 1000 + now.tv_usec;
}
void thread_proc()
{
for(int i=0; i<100000000; ++i){
pthread_spin_lock(&spin_lock);
++num;
pthread_spin_unlock(&spin_lock);
}
}
int main()
{
pthread_spin_init(&spin_lock, PTHREAD_PROCESS_PRIVATE);//maybe PHREAD_PROCESS_PRIVATE or PTHREAD_PROCESS_SHARED
int64_t start = get_current_timestamp();
std::thread t1(thread_proc), t2(thread_proc);
t1.join();
t2.join();
std::cout<<"num:"<<num<<std::endl;
int64_t end = get_current_timestamp();
std::cout<<"cost:"<<end-start<< " us" <<std::endl;
pthread_spin_destroy(&spin_lock);
return 0;
}
结果分析
自旋锁运行效率快一些
改一下循环次数
for(int i=0; i<100000000; ++i){
pthread_spin_lock(&spin_lock);
++num;
for(int i=0; i<100; ++i){
//do nothing
}
pthread_spin_unlock(&spin_lock);
}
自旋锁运行效率慢一些,spin lock虽然mutex的性能更好(花费很少的CPU指令),但是它只适应于临界区运行时间很短的场景。
读写锁
读者写者问题
有一群写者和一群读者,写者在写同一本书,读者也在读这本书,多个读者可以同时读这本书,但是,只能有一个写者在写书。特征:
- 任意多的读进程可以同时读这个文件;
- 一次只允许一个写进程往文件中写;
- 如果一个写进程正在往文件中写,禁止任何读进程或写进程访问文件;
- 写进程执行写操作前,应让已有的写者或读者全部退出。这说明当有读者在读文件时不允许写者写文件。
有时候,在多线程中,有一些公共数据修改的机会比较少,而读的机会却是非常多的,此公共数据的操作基本都是读,如果每次操作都给此段代码加锁,太浪费时间了而且也很浪费资源,降低程序的效率,因为读操作不会修改数据,只是做一些查询,所以在读的时候不用给此段代码加锁,可以共享的访问,只有涉及到写的时候,互斥的访问就好了
定义
读写锁是一种特殊的自旋锁,它把对共享资源对访问者划分成了读者和写者,读者只对共享资源进行访问,写者则是对共享资源进行写操作。读写锁一个读写锁同时只能存在一个写锁但是可以存在多个读锁,但不能同时存在写锁和读锁。
三种状态
如果读写锁当前没有读者,也没有写者,那么写者可以立刻获的读写锁,否则必须自旋,直到没有任何的写锁或者读锁存在。如果读写锁没有写锁,那么读锁可以立马获取,否则必须等待写锁释放。
所以读写锁具备三种状态:
- 读模式下加锁状态 (读锁)
- 写模式下加锁状态 (写锁)
- 不加锁状态
特性
- 读写锁是"写模式加锁"时, 解锁前,所有对该锁加锁的线程都会被阻塞;
- 读写锁是"读模式加锁"时, 如果线程以读模式对其加锁会成功;如果线程以写模式加锁会阻塞;
- 读写锁是"读模式加锁"时, 既有试图以写模式加锁的线程,也有试图以读模式加锁的线程。那么读写锁会阻塞随后的读模式锁请求。优先满足写模式锁。读锁、写锁并行阻塞,写锁优先级高。
写独占,读共享;写锁优先级高
使用流程(相关API)
pthread_rwlock_init()
作用
初始化
函数原型
int pthread_rwlock_init(pthread_rwlock_t *restrict rwlock,const pthread_rwlockattr_t *restrict attr);
参数
- pthread_rwlock_t:定义读写锁的结构体
- PTHREAD_RWLOCK_INITIALIZER静态初始化读写锁
- pthread_rwlock_init 函数动态初始化锁
- attr:通常使用默认属性,NULL
pthread_rwlock_t rwlock;
pthread_rwlock_init(&rwlock, NULL);
返回值
成功返回0, 失败直接返回错误号
pthread_rwlock_rdlock() (阻塞)
作用
申请读锁
函数原型
int pthread_rwlock_rdlock(pthread_rwlock_t *rwlock);
返回值
成功,返回 0。否则,将返回用于指明错误的错误号
注意事项
- 如果写入器未持有读锁,并且没有任何写入器基于该锁阻塞,则调用线程会获取读锁。
- 如果写入器未持有读锁,但有多个写入器正在等待该锁时,调用线程是否能获取该锁是不确定的。
- 如果一个线程写锁定了 读写锁后,又调用了pthread_rwlock_rdlock来读锁定同一个读写锁,结果无法预测。
- 如果某个写入器持有读锁,则调用线程无法获取该锁。
- 调用线程必须获取该锁之后,才能从 pthread_rwlock_rdlock() 返回。
- 为避免写入器资源匮乏,允许在多个实现中使写入器的优先级高于读取器。
- pthread_rwlock_rdlock() n 次。该线程必须调用pthread_rwlock_unlock() n 次才能执行匹配的解除锁定操作。
- 线程信号处理程序可以处理传送给等待读写锁的线程的信号。从信号处理程序返回后,线程将继续等待读写锁以执行读取,就好像线程未中断一样。
pthread_rwlock_tryrdlock()(非阻塞)
作用
申请读锁
函数原型
int pthread_rwlock_tryrdlock(pthread_rwlock_t *rwlock);//尝试加读锁,没锁上就立即返回
返回值
无论加锁是否成功,上面的函数都会立即返回,成功返回 0,失败返回 EBUSY。
注意事项
如果任何线程持有 rwlock 中的写锁或者写入器基于 rwlock 阻塞,则 pthread_rwlock_tryrdlock()
函数会失败。
pthread_rwlock_wrlock() (非阻塞)
作用
申请写锁
函数原型
int pthread_rwlock_wrlock(pthread_rwlock_t *rwlock);
返回值
成功返回0, 失败直接返回错误号
注意事项
- 如果没有其他读取器线程或写入器线程持有读写锁 rwlock,则调用线程将获取写锁,否则,调用线程将阻塞。
- 调用线程必须获取该锁之后,才能从 pthread_rwlock_wrlock() 调用返回。
- 如果在进行调用时,调用线程持有读写锁(读锁或写锁),则结果是不确定的。
- 为避免写入器资源匮乏,写入器的优先级高于读取器。 如果针对未初始化的读写锁调用 pthread_rwlock_wrlock(),则结果是不确定的。
- 线程信号处理程序可以处理传送给等待读写锁以执行写入的线程的信号。从信号处理程序返回后,线程将继续等待读写锁以执行写入,就好像线程未中断一样。
pthread_rwlock_trywrlock()(非阻塞)
作用
申请写锁
函数原型
int pthread_rwlock_trywrlock(pthread_rwlock_t *rwlock);//尝试(没锁上就立即返回)加锁
返回值
成功返回0, 失败直接返回错误号
注意事项
- 如果任何线程当前持有用于读取和写入的 rwlock,则pthread_rwlock_trywrlock() 函数会失败。
- 如果针对未初始化的读写锁调用 pthread_rwlock_trywrlock(),则结果是不确定的
pthread_rwlock_unlock()
作用
释放在 rwlock 引用的读写锁对象中持有的锁。
函数原型
int pthread_rwlock_unlock (pthread_rwlock_t *rwlock);
返回值
成功返回 0,否则返回用于指明错误的错误号
注意事项
- 如果调用线程未持有读写锁 rwlock,则结果是不确定的。
- 如果通过调用 pthread_rwlock_unlock()来释放读写锁对象中的读锁,并且其他读锁当前由该锁对象持有,则该对象会保持读取锁定状态。
- 如果 pthread_rwlock_unlock()释放了调用线程在该读写锁对象中的最后一个读锁,则调用线程不再是该对象的属主。
- 如果 pthread_rwlock_unlock()释放了该读写锁对象的最后一个读锁,则该读写锁对象将处于无属主、解除锁定状态。
- 如果通过调用 pthread_rwlock_unlock() 释放了该读写锁对象的最后一个写锁,则该读写锁对象将处于无属主、解除锁定状态。
- 如果 pthread_rwlock_unlock()解除锁定该读写锁对象,并且多个线程正在等待获取该对象以执行写入,则通过调度策略可确定获取该对象以执行写入的线程。
- 如果多个线程正在等待获取读写锁对象以执行读取,则通过调度策略可确定等待线程获取该对象以执行写入的顺序。
- 如果多个线程基于rwlock 中的读锁和写锁阻塞,则无法确定读取器和写入器谁先获得该锁。
- 如果针对未初始化的读写锁调用 pthread_rwlock_unlock(),则结果是不确定的。
pthread_rwlock_destroy()
作用
销毁 rwlock 引用的读写锁对象并释放该锁使用的任何资源
函数原型
int pthread_rwlock_destroy(pthread_rwlock_t *rwlock);
返回值
成功,返回 0。否则,返回用于指明错误的错误号
注意事项
- 尝试销毁未初始化的读写锁会产生不确定的行为。
- 已销毁的读写锁对象可以使用 pthread_rwlock_init() 来重新初始化。
- 销毁读写锁对象之后,如果以其他方式引用该对象,则结果是不确定的。
测试代码
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <unistd.h>
pthread_rwlock_t rwlock;//创建读写锁
long int love;
void *pth_wr(void *arg)//写操作
{
int i = (int)arg;//参数类型转化
while (love <= 520)
{
pthread_rwlock_wrlock(&rwlock);//请求写锁
printf("write================love = %ld, threadID = %d\n", love += 40, i + 1);//写操作,love每次加40
pthread_rwlock_unlock(&rwlock);//写锁释放
sleep(1);
}
return NULL;
}
void *pth_rd(void *arg)//读操作
{
int i = (int)arg;
while (love <= 520)
{
pthread_rwlock_rdlock(&rwlock);//请求读锁
printf("love = %ld, threadID = %d-------------------- read\n", love, i + 1);
pthread_rwlock_unlock(&rwlock);//读锁释放
sleep(1);
}
return NULL;
}
int main(void)
{
pthread_t pth[10];
int i;
pthread_rwlock_init(&rwlock, NULL);
for (i = 0; i != 5; i++)
{
pthread_create(&pth[i], NULL, pth_wr, (void *)i);
}
for (i = 0; i != 5; i++)
{
pthread_create(&pth[5 + i], NULL, pth_rd, (void *)i);
}
while (1)
{
if (love >= 520)
{
for (int j = 0; j != 10; j++)
{
pthread_join(pth[j], NULL);
}
break;
}
}
pthread_rwlock_destroy(&rwlock);
return 0;
}
互斥锁、读写锁、自旋锁区别
互斥锁:
- 用于保证在任何时刻,都只能有一个线程访问该对象。当获取锁操作失败时,线程会进入睡眠,等待锁释放时被唤醒
读写锁:
- 分为读锁和写锁。处于读操作时,可以允许多个线程同时获得读操作。但是同一时刻只能有一个线程可以获得写锁。其它获取写锁失败的线程都会进入睡眠状态,直到写锁释放时被唤醒。 注意:写锁会阻塞其它读写锁。当有一个线程获得写锁在写时,读锁也不能被其它线程获取;写者优先于读者(一旦有写者,则后续读者必须等待,唤醒时优先考虑写者)。适用于读取数据的频率远远大于写数据的频率的场合。
自旋锁:
- 在任何时刻同样只能有一个线程访问对象。但是当获取锁操作失败时,不会进入睡眠,而是会在原地自旋,直到锁被释放。这样节省了线程从睡眠状态到被唤醒期间的消耗,在加锁时间短暂的环境下会极大的提高效率。但如果加锁时间过长,则会非常浪费CPU资源。
读写锁的三种实现
由于C++14和C++17 均对读写锁进行了函数封装,但是这里的是C++11实现读写锁
使用互斥锁和条件变量实现读写锁
class readwrite_lock
{
public:
readwrite_lock()
: stat(0)
{
}
void readLock()
{
mtx.lock();
while (stat < 0)
cond.wait(mtx);
++stat;
mtx.unlock();
}
void readUnlock()
{
mtx.lock();
if (--stat == 0)
cond.notify_one(); // 叫醒一个等待的写操作
mtx.unlock();
}
void writeLock()
{
mtx.lock();
while (stat != 0)
cond.wait(mtx);
stat = -1;
mtx.unlock();
}
void writeUnlock()
{
mtx.lock();
stat = 0;
cond.notify_all(); // 叫醒所有等待的读和写操作
mtx.unlock();
}
private:
mutex mtx;
condition_variable cond;
int stat; // == 0 无锁;> 0 已加读锁个数;< 0 已加写锁
};
使用2个互斥锁实现读写锁
class readwrite_lock
{
public:
readwrite_lock()
: read_cnt(0)
{
}
void readLock()
{
read_mtx.lock();
if (++read_cnt == 1)
write_mtx.lock();
read_mtx.unlock();
}
void readUnlock()
{
read_mtx.lock();
if (--read_cnt == 0)
write_mtx.unlock();
read_mtx.unlock();
}
void writeLock()
{
write_mtx.lock();
}
void writeUnlock()
{
write_mtx.unlock();
}
private:
mutex read_mtx;
mutex write_mtx;
int read_cnt; // 已加读锁个数
};
用mutex和conditon实现写优先的读写锁
class RWLock {
private:
pthread_mutex_t mxt;
pthread_cond_t cond;
int rd_cnt;//等待读的数量
int wr_cnt;//等待写的数量
public:
RWLock() :rd_cnt(0), wr_cnt(0) {
pthread_mutex_init(&mxt,NULL);
pthread_cond_init(&cond,NULL);
}
void readLock() {
pthread_mutex_lock(&mxt);
++rd_cnt;
while(wr_cnt > 0)
pthread_mutex_wait(&cond, &mxt);
pthread_mutex_unlock(&mxt);
}
void readUnlock() {
pthread_mutex_lock(&mxt);
--rd_cnt;
if (rd_cnt == 0 )
pthread_cond_signal(&cond);
pthread_mutex_unlock(&mxt);
}
void writeLock() {
pthread_mutex_lock(&mxt);
++wr_cnt;
while (wr_cnt + rd_cnt >=2)
pthread_cond_wait(&cond, &mxt);
pthread_mutex_unlock(&mxt);
}
void writerUnlock() {
pthread_mutex_lock(&mxt);
--wr_cnt;
if(wr_cnt==0)
pthread_cond_signal(&cond);
pthread_mutex_unlock(&mxt);
}
};