02Linux下C语言锁的学习之Linux下的读写锁
概述:
下面的锁的意思均是代表读写锁。
读写锁的特性:
-
1)若一把锁被一个线程以读方式锁住,当其它线程以读方式上锁的话,那么可以上锁成功。
-
2)若一把锁被一个线程以写方式锁住,当其它线程以读或者写方式上锁的话,都会被阻塞。
-
要注意的是:多个线程同时请求锁时,请求读方式的线程会被放在请求写方式的线程后面;若不是同时请求上锁,读方式先请求的则先等已经上锁的线程处理后再处理(当然如果该线程以读方式上锁可以同时进行),即使后面再有写请求也要等其完成才能上锁。 这里的同时和不是同时很重要。
-
实际上上面的意思可以总结为:
读时共享,写时阻塞(独占),写锁优先级高,注意同时与非同时字眼,注意目前以何种方式上锁。//记住这五句话就可以完全吃透读写锁的各个线程的执行顺序了。具体可以看下面的场景分析。
有点类似fork创建子进程时继承父进程的全局变量。
1 与Linux下的读写锁相关的函数介绍
1)pthread_rwlock_init函数
int pthread_rwlock_init(pthread_rwlock_t *restrict rwlock, const pthread_rwlockattr_t *restrict attr);
/*
功能:初始化一把锁。
参1:读写锁。
参2:读写锁属性,通常使用默认属性,传NULL即可。
*/
2)pthread_rwlock_destroy函数
int pthread_rwlock_destroy(pthread_rwlock_t *rwlock);
/*
功能:销毁一把锁。
参1:读写锁。
*/
3)pthread_rwlock_rdlock函数
int pthread_rwlock_rdlock(pthread_rwlock_t *rwlock);
/*
功能:以只读方式锁住读写锁。
参1:读写锁。
*/
4)pthread_rwlock_wrlock函数
int pthread_rwlock_wrlock(pthread_rwlock_t *rwlock);
/*
功能:以读写方式上锁,简称叫以写方式上锁。
参1:读写锁。
*/
5)pthread_rwlock_unlock函数
int pthread_rwlock_unlock(pthread_rwlock_t *rwlock);
/*
功能:解锁。
参1:读写锁。
*/
6)pthread_rwlock_tryrdlock函数
int pthread_rwlock_tryrdlock(pthread_rwlock_t *rwlock);
/*
功能:以读方式尝试请求读写锁(非阻塞请求读锁)。
参1:读写锁。
*/
7)pthread_rwlock_trywrlock函数
int pthread_rwlock_trywrlock(pthread_rwlock_t *rwlock);
/*
功能:以写方式尝试请求读写锁(非阻塞请求写锁)
参1:读写锁。
*/
2 读写锁的场景分析
1)场景一:均是读请求。
当下面4个线程请求读写锁时,不管是否是同时,都能成功上锁访问数据。
2)场景2:T1,T2线程同时上锁,T3,T4在后面请求。
由于T1,T2是同时请求上锁,但写请求优先级高,所以T2先上锁,其它线程阻塞。T2线程处理完后T1,T3,T4均能上锁,上锁的顺序看CPU先处理哪个请求,不需要考虑顺序。
3)场景3:T1,T2在以读方式上锁时,T3,T4以写方式请求。
因为T1,T2已经上锁,所以T3,T4会阻塞等待,即使你是以写方式请求。
写请求优先级高是针对于没有上锁且是同时请求的情况下。
- 注意:当一把锁被写方式锁住时,读方式请求先到,然后再以写方式请求,由于锁被锁住,所以这种也认为是同时,写方式请求将被放在读方式请求之前。
当一把锁被读方式锁住时不需要考虑,因为再以读方式先请求的话会共享,写方式再请求因读方式请求已经共享,所以实际剩下自己,已经无对比可言。
4)场景4:T1先以读方式加锁成功,后面三个线程同时请求。
这里再提醒一下:若T1先加锁成功,在操作期间,T2,T3,T4即使按不同顺序(除了T2放在前并且CPU开始处理该请求,否则即使T2先请求但是还在队列时T3请求来了那么顺序发生改变,T2即使在最前同样不可以共享),也是认为是同时(看场景3的解释),因为T1还在操作期间嘛。
当T1在操作期间,由于是同时请求并且写优先级高,那么T3一定会被排在T2,T4线程之后,所以T3不可能共享,必须等到T2,T4处理完后才能访问。
3 读写锁代码例子
代码很简单,创建了3个写线程,5个读线程。共享资源为int型全局变量。
/* 3个线程不定时 "写" 全局资源,5个线程不定时 "读" 同一全局资源 */
#include <stdio.h>
#include <unistd.h>
#include <pthread.h>
int counter; //全局资源
pthread_rwlock_t rwlock;
//写线程的回调函数
void *th_write(void *arg)
{
int t;
int i = (int)arg;
while (1) {
//写线程期间,独占
pthread_rwlock_wrlock(&rwlock);
t = counter;
usleep(1000);//模拟业务,让读线程获取CPU,但是读线程因为锁被锁住而阻塞
printf("=======write %d: %lu: counter=%d ++counter=%d\n", i, pthread_self(), t, ++counter);
pthread_rwlock_unlock(&rwlock);
usleep(5000);//让其它线程获取锁,防止本线程回到while又重新上锁,导致其它线程很少机会获取到锁
}
return NULL;
}
//读线程的回调函数
void *th_read(void *arg)
{
//区别线程几的因子
int i = (int)arg;
while (1) {
//读线程期间,读时共享
pthread_rwlock_rdlock(&rwlock);
printf("----------------------------read %d: %lu: %d\n", i, pthread_self(), counter);
pthread_rwlock_unlock(&rwlock);
usleep(900);//让其它线程获取锁,防止本线程回到while又重新上锁,导致其它线程很少机会获取到锁
}
return NULL;
}
//这里为了简单没做错误分析
int main(void)
{
int i;
pthread_t tid[8];//线程id
pthread_rwlock_init(&rwlock, NULL);
//创建3个写线程
for (i = 0; i < 3; i++)
pthread_create(&tid[i], NULL, th_write, (void *)i);
//创建5个读线程
for (i = 0; i < 5; i++)
pthread_create(&tid[i+3], NULL, th_read, (void *)i);
//回收子线程资源
for (i = 0; i < 8; i++)
pthread_join(tid[i], NULL);
//释放读写琐
pthread_rwlock_destroy(&rwlock);
return 0;
}
上面程序会不断打印count被写和读共享时的值。
截图部分分析结果:
1)下图看到,问:write 0:4150012746那行在写期间,黑色部分的上锁请求可能同时进行吗?
答:不可能,如果同时请求,那么根据写请求优先,write 2那行必定在前面的read前面,所以write 2是在那10个读线程之后请求的。
2)问:蓝色框里面的请求有可能是一起来的吗?
答:就是一起同时来的,然后根据优先级写请求放在前面。因为根据write 2上面是读可以知道,如果不是一起来的话,write 1下面的读请求先于write 2先来,那么是可以共享的,应该在write 2前共享。
4 总结读写锁
由于读写锁在实际应用很少用,并且麻烦,所以不建议大家使用,并且一般互斥锁就够Linux下的C程序员使用了。这里只是让大家有这个概念。要搞明白读写锁如何排序线程的请求,记住概述的那五句话即可,都是按照那五句话进行排序请求。
读写锁完结。