简 述: 上一篇中介绍了多线程使用互斥量(锁)来控制程序的访问公共资源的时候是”串行“的;本篇继续,重点讲解如下几个概念:Linux 中的原子操作死锁原因及解决方法 、和读写锁 和对应的源码小例子。其中读写锁的使用例子,完全可以参考互斥量(锁),其大概流程如下:

  • pthread_rwlock_init()
  • pthread_rwlock_rdlock() / pthread_rwlock_tryrdlock() / pthread_rwlock_wrlock() / pthread_rwlock_trywrlock()
  • 、、、代码片
  • pthread_rwlock_unlock()
  • pthread_rwlock_destroy()

[TOC]


本文初发于 “偕臧的小站“,同步转载于此。


编程环境:

  💻: uos20 📎 gcc/g++ 8.3 📎 gdb8.0

  💻: MacOS 10.14 📎 gcc/g++ 9.2 📎 gdb8.3


原子操作:

  • 原子操作:

    • cpu 处理一个指令,线程 / 进程在处理完这个指令之前,是不会失去 cpu 的。

借用显示生活中的知识,原子⚛是最小的不可分割的物质,没有比它更小的(类比,不详探究夸克);在一个程序中,是有几百行代码构成了,可以将一行代码(一行表达式语句)看做为一个 ”原子操作“

比如:

printf("");
int a = b + 100;

  • 临界区:

    • 从代码的角度理解,就是 执行加锁语句 pthread_mutex_lock() 和解锁语句 pthread_mutex_unlock() 之间代码片,称之为 临界区; 也可以看作为 ”伪原子操作“ ,因为它有可能临界区的代码执行到一半,cpu 就被抢走了,但是其虽然抢到了 cpu 但是会阻塞,或者不能够访问该临界区的代码片,然后等待轮转,cpu 再次回来,继续在自己身上继续执行接下来的代码行;然后这样临界区的代码就只有它执行完毕了。可以看做是一个 ”伪“ 原子操作。

    示意图如下:


造成死锁的原因:

自己锁自己:

  • 分析: 当遇到连续锁两次的时候,线程会阻塞在 第二个 pthread_mutex_lock() 函数这一行里面。

循环锁住:


避免死锁的方式:

避免或者解决死锁的三种方式如下:

  • 让线程按照一定的顺序访问共享资源
  • 在访问其他锁的时候,需要先将自己的锁解开
  • 设置上锁的使用,可以使用 pthread_mutex_trylock() 函数

读写锁:

除了使用互斥量(锁)之外,还可以采用 读写锁 来控制多线程访问共享资源。

读写锁的理解:

  • 读锁 - 对内存做读操作
  • 写锁 - 对内存做写操作

读写锁的特性:

  • 线程 A 加锁成功,又来了三个线程,做读操作,可以加锁成功
    • 读共享 - 并行处理
  • 线程 A 加写锁成功,又来了三个线程,做读操作,三个线程阻塞
    • 写独占
  • 线程 A 加读锁成功,又来了 B 线程加写锁线程阻塞,又来了 C 线程加读锁阻塞
    • 读写不能同时进行
    • 写的优先级高(即使后面线程有先后来顺序来,也会看一下优先级)

读写锁的场景练习:

上面的读写锁的特性 可以看做是理论部分,然后这里用几个实际场景进行一下分析:

  • 线程 A 加写锁成功,线程 B 请求读锁
    • 线程 B 阻塞
  • 线程 A 持有读锁,线程 B 请求写锁
    • 线程 B 阻塞
  • 线程 A 拥有读锁,线程 B 请求读锁
    • 线程 B 加锁成功
  • 线程 A 持有读锁,然后线程 B 请求写锁,然后线程 C 请求读锁
    • B 阻塞,C 阻塞 -写的优先级高
    • A 解锁,B 线程加写锁成功,C继续阻塞
    • B 解锁,C 加读锁成功
  • 线程A持有写锁,然后线程B请求读锁,然后线程C请求写锁

读写锁的使用场景:

  • 互斥锁 - 读写串行
  • 读写锁:
    • 读:并行
    • 写:串行
  • 程序中的 “读操作” 大于 ”写操作“ 的时候,比如说 12306 买火车票的例子 ,就有大量的率新读取数据,远大于买票的时候写操作。

读写锁的主要操作函数:

读写锁的使用流程和互斥量(锁)的流程基本一样。

  • 初始化读写锁

    int pthread_rwlock_init(pthread_rwlock_t *lock, const pthread_rwlockattr_t *attr);
  • 销毁读写锁

    int pthread_rwlock_destroy(pthread_rwlock_t *lock);
  • 加读锁

    int pthread_rwlock_rdlock(pthread_rwlock_t *lock);
  • 尝试加读锁

    int pthread_rwlock_tryrdlock(pthread_rwlock_t *lock);
  • 加写锁

    int pthread_rwlock_wrlock(pthread_rwlock_t *lock);
  • 尝试加写锁

    int pthread_rwlock_trywrlock(pthread_rwlock_t *lock);
  • 解锁

    int pthread_rwlock_unlock(pthread_rwlock_t *lock);

写一个运用读写锁的例子:

上面例子讲了这么多用法和属性作为铺垫,这里有一代码例子,讲解读写锁的使用例子:

  • 需求练习:

    • 3 个线程不定时写同一全局资源,5 个线程不定时读同一全局资源
  • 代码:

    #include <stdio.h>
    #include <unistd.h>
    #include <pthread.h>
    
    int g_number = 0;
    pthread_rwlock_t lock;
    
    void* writeFunc(void* arg);
    void* readFunc(void* arg);
    
    int main(int argc, char *argv[])
    {
        pthread_t p[8];
    
        pthread_rwlock_init(&lock, nullptr); //初始化一个锁
    
        for (int i = 0; i < 3; i++) {  //创建写线程
            pthread_create(&p[i], nullptr, writeFunc, nullptr);
        }
    
        for (int i = 3; i < 8; i++) {  //创建读线程
            pthread_create(&p[i], nullptr, readFunc, nullptr);
        }
    
        for (int i = 0; i < 8; i++) {  //阻塞回收子线程的 pcb
            pthread_join(p[i], nullptr);
        }
    
        pthread_rwlock_destroy(&lock); //销毁读写锁,释放锁资源
        
        return 0;
    }
    
    void* writeFunc(void* arg)
    {
        while (true) {
            pthread_rwlock_wrlock(&lock); //加写锁
            g_number++;
            printf("--write: %lu, %d\n", pthread_self(), g_number);
            pthread_rwlock_unlock(&lock);  //解锁
            usleep(500);
        }
        
        return nullptr;
    }
    
    void* readFunc(void* arg)
    {
        while (true) {
            pthread_rwlock_rdlock(&lock);  //加读锁
            printf("--read : %lu, %d\n", pthread_self(), g_number);
            pthread_rwlock_unlock(&lock);  //解锁
            usleep(500);
        }
    
        return nullptr;
    }
  • 运行效果:

    • 屏蔽去掉加锁解锁的几行注释,则会出现以下异常情况,小数可能在大数后面再执行打印语句:

    • 加上读写锁之后,得到预期正确结果,大数只会出现在小数后面打印


下载地址:

19_pthread_rwlock_wrlock

欢迎 star 和 fork 这个系列的 linux 学习,附学习由浅入深的目录。