简 述: 上一篇中介绍了多线程使用互斥量(锁)来控制程序的访问公共资源的时候是”串行“的;本篇继续,重点讲解如下几个概念: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; }
运行效果:
屏蔽去掉加锁解锁的几行注释,则会出现以下异常情况,小数可能在大数后面再执行打印语句:
加上读写锁之后,得到预期正确结果,大数只会出现在小数后面打印
下载地址:
欢迎 star 和 fork 这个系列的 linux 学习,附学习由浅入深的目录。