多线程读写锁应用

实际场景

大多数情况下,线程只是读取共享变量的值,并不修改;只有少数情况下,线程才真正修改共享变量的值

"读" & "写" 场景下的临界区保护

多个线程同时读取一个共享变量时,并不存在冲突 (无须保护)

只有在线程写共享变量时,才需要对其进行保护

问题:互斥量可用于多线程 "读" & "写" 共享变量的场景吗?

互斥量是一种普适的解决方案,其缺点是效率不够出色

因此,针对多线程 "读" & "写" 场景需要更具针对性的解决方案

针对 "读" & "写" 的特殊锁

读锁:允许多个线程同时访问共享变量 (读取值)

写锁:只允许一个线程单独访问共享变量 (改变值)

问题:

什么时候用 "读锁"?什么时候用 "写锁"?

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 上去运行

程序运行结果如下图所示:

可以看出主线程会一直获取到写锁,子线程无法获取到读锁

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值