实际场景
大多数情况下,线程只是读取共享变量的值,并不修改;只有少数情况下,线程才真正修改共享变量的值
"读" & "写" 场景下的临界区保护
多个线程同时读取一个共享变量时,并不存在冲突 (无须保护)
只有在线程写共享变量时,才需要对其进行保护
问题:互斥量可用于多线程 "读" & "写" 共享变量的场景吗?
互斥量是一种普适的解决方案,其缺点是效率不够出色
因此,针对多线程 "读" & "写" 场景需要更具针对性的解决方案
针对 "读" & "写" 的特殊锁
读锁:允许多个线程同时访问共享变量 (读取值)
写锁:只允许一个线程单独访问共享变量 (改变值)
问题:
什么时候用 "读锁"?什么时候用 "写锁"?
Linux 中的读写锁 API 函数
读写锁示例程序
读写锁应用要点
保护临界区时,必须清楚当前需要使用 "读锁" 还是 "写锁"
无论是 "读锁" 还是 "写锁",解锁都是调用 pthread_rwlock_unlock()
- 如果所有线程都使用 "读锁",则临界区失去保护
- 如果所有线程都使用 "写锁",则读写锁退化为互斥量
读写锁应用初体验
test-1.c
#define _GNU_SOURCE /* To get pthread_getattr_np() declaration */
#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <memory.h>
#include <time.h>
#define NLOOP_FOR_ESTIMATION 1000000000UL
#define NSECS_PER_MSEC 1000000UL
#define NSECS_PER_SEC 1000000000UL
#define TEST_LOAD 10000000
#define DiffNS(begin, end) ((end.tv_sec - begin.tv_sec) * NSECS_PER_SEC \
+ (end.tv_nsec - begin.tv_nsec))
int g_count = 0;
pthread_rwlock_t g_lock = PTHREAD_RWLOCK_INITIALIZER;
void* thread_read(void* arg)
{
int i = (long)arg;
while( 1 )
{
pthread_rwlock_rdlock(&g_lock);
printf("%s(%d) : %d\n", __FUNCTION__, i, g_count);
pthread_rwlock_unlock(&g_lock);
usleep(100 * 1000);
}
pthread_detach(pthread_self());
return NULL;
}
int main()
{
long i = 0;
pthread_t t[5] = {0};
int len = sizeof(t)/sizeof(*t);
for(i=0; i<len; i++)
{
pthread_create(t+i, NULL, thread_read, (void*)i);
}
printf("Hello World!\n");
while( 1 )
{
pthread_rwlock_wrlock(&g_lock);
printf("%s : %d\n", __FUNCTION__, ++g_count);
pthread_rwlock_unlock(&g_lock);
sleep(1);
}
return 0;
}
主线程创建了 5 个子线程,子线程通过读写锁中的读锁来读取临界资源 g_count,而主线程通过读写锁中的写锁来改变临界资源 g_count
使用读写锁,多个线程可同时读取临界资源的值;当同时使用读锁和写锁时,读锁和写锁会产生互斥
程序运行结果如下图所示:
通过读写锁也可以来访问临界资源
问题
如果两个线程 "同时" 拿到了 "读锁" 和 "写锁",问:哪个线程先进入临界区执行?
读优先 or 写优先
读优先:拿到 "读锁" 的线程优先进入临界区
写优先:拿到 "写锁" 的线程优先进入临界区
读写优先 API 函数
读写锁深度剖析
读写锁在实现上比互斥量复杂,单纯的 "上锁" 和 "解锁" 操作读写锁相对互斥量低效
例:读写场景,如果临界区比较短小,且并发量不大,读写锁并不比互斥量高效
即便只有一个读写锁可也能导致死锁:
下面的程序是否存在问题?
读写锁深度实验
test-2.c
#define _GNU_SOURCE /* To get pthread_getattr_np() declaration */
#define _XOPEN_SOURCE >= 500 || _POSIX_C_SOURCE >= 200809L
#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <memory.h>
#include <time.h>
#define NLOOP_FOR_ESTIMATION 1000000000UL
#define NSECS_PER_MSEC 1000000UL
#define NSECS_PER_SEC 1000000000UL
#define TEST_LOAD 10000000
#define DiffNS(begin, end) ((end.tv_sec - begin.tv_sec) * NSECS_PER_SEC \
+ (end.tv_nsec - begin.tv_nsec))
int g_count = 0;
pthread_rwlock_t g_lock = PTHREAD_RWLOCK_INITIALIZER;
void* thread_read(void* arg)
{
int i = (long)arg;
pthread_rwlock_rdlock(&g_lock);
printf("%s(%d) : %d\n", __FUNCTION__, i, g_count);
sleep(3);
pthread_rwlock_rdlock(&g_lock);
printf("%s(%d) : %d\n", __FUNCTION__, i, g_count);
pthread_rwlock_unlock(&g_lock);
pthread_rwlock_unlock(&g_lock);
return NULL;
}
int main()
{
long i = 0;
pthread_t t[50] = {0};
int len = sizeof(t)/sizeof(*t);
pthread_rwlockattr_t rwattr;
int pref = PTHREAD_RWLOCK_PREFER_WRITER_NONRECURSIVE_NP;
pthread_rwlockattr_init(&rwattr);
pthread_rwlockattr_setkind_np(&rwattr, pref);
pthread_rwlock_init(&g_lock, &rwattr);
pthread_create(t, NULL, thread_read, (void*)i);
printf("Hello World!\n");
sleep(1);
pthread_rwlock_wrlock(&g_lock);
sleep(1);
printf("%s : %d\n", __FUNCTION__, ++g_count);
pthread_rwlock_unlock(&g_lock);
printf("End!\n");
return 0;
}
如果两个线程同时获取读锁和写锁,那么默认的行为是读优先,第 51 行,我们把读写锁的策略改为写优先
首先是子线程获取读锁,然后主线程尝试获取写锁,但由于子线程没有释放读锁,所以主线程会阻塞,子线程休眠 3s 后,又去获取读锁,但由于是写优先,所以子线程要等待主线程获取到写锁,然后才能获取读锁,但子线程之前获取到的读锁没有释放,所以导致了死锁
同时多次获取读锁不会有问题,如果将 51 行注释掉,读写锁的策略为读优先,则程序不会发生死锁
程序运行结果如下图所示:
程序发生了死锁
test-3.c
#define _GNU_SOURCE /* To get pthread_getattr_np() declaration */
#define _XOPEN_SOURCE >= 500 || _POSIX_C_SOURCE >= 200809L
#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <memory.h>
#include <time.h>
#define NLOOP_FOR_ESTIMATION 1000000000UL
#define NSECS_PER_MSEC 1000000UL
#define NSECS_PER_SEC 1000000000UL
#define TEST_LOAD 10000000
#define DiffNS(begin, end) ((end.tv_sec - begin.tv_sec) * NSECS_PER_SEC \
+ (end.tv_nsec - begin.tv_nsec))
int g_count = 0;
pthread_rwlock_t g_lock = PTHREAD_RWLOCK_INITIALIZER;
void* thread_read(void* arg)
{
int i = (long)arg;
while( 1 )
{
int v = 0;
pthread_rwlock_rdlock(&g_lock);
printf("%s(%d) : %d\n", __FUNCTION__, i, g_count);
v = g_count;
pthread_rwlock_unlock(&g_lock);
if( v == TEST_LOAD ) break;
}
return NULL;
}
int main()
{
long i = 0;
pthread_t t[5] = {0};
int len = sizeof(t)/sizeof(*t);
pthread_rwlockattr_t rwattr;
int pref = PTHREAD_RWLOCK_PREFER_WRITER_NONRECURSIVE_NP;
pthread_rwlockattr_init(&rwattr);
pthread_rwlockattr_setkind_np(&rwattr, pref);
pthread_rwlock_init(&g_lock, &rwattr);
for(i=0; i<len; i++)
{
pthread_create(t, NULL, thread_read, (void*)i);
}
printf("Hello World!\n");
sleep(1);
for(i=0; i<TEST_LOAD; i++)
{
pthread_rwlock_wrlock(&g_lock);
printf("%s : %d\n", __FUNCTION__, ++g_count);
pthread_rwlock_unlock(&g_lock);
}
for(i=0; i<len; i++)
{
pthread_join(t[i], NULL);
}
printf("End!\n");
return 0;
}
第 53 行,我们把读写锁的策略改为写优先
在主线程中,我们创建了 5 个子线程通过读锁来读取临界资源 g_count 的值,并打印;主线程在休眠 1s 后,会去不断的获取写锁来改变临界资源 g_count 的值,由于是写优先,并且主线程在循环获取写锁,所以子线程中就一直无法获取读锁,这样会导致子线程饿死,无法继续向下执行
我们使用 taskset -c 0 ./a.out 命令,让这个程序只能在 cpu0 上去运行
程序运行结果如下图所示:
可以看出主线程会一直获取到写锁,子线程无法获取到读锁