1. 概述
-
线程同步
当有一个线程在对内存进行操作时,其他线程都不可以对这个内存地址进行操作,直到该线程完成操作。
多个线程访问共享资源(全局数据区,堆区,文件描述符),按先后顺序 -
临界资源
多个线程共同访问的数据–>全局数据区数据,堆区数据
-
临界区
在程序中有一个代码块在这个代码块中每行代码都对共享数据进行了读或者写操作
实现线程同步的方式:互斥锁,读写锁,条件变量+锁,信号量+锁
2. 互斥锁
2.1 互斥锁类型/特点
-
类型
pthread_mutex_t mutex
-
特点
线程对临界区的访问是串行的, 访问效率低
-
互斥锁相关API
#include <pthread.h> // 初始化互斥锁, 初始化完毕之后, 互斥锁的打开的 int pthread_mutex_init(pthread_mutex_t *restrict mutex, const pthread_mutexattr_t *restrict attr); 参数: - mutex: 要初始化的互斥锁的地址 - attr: 互斥锁的属性, 一般使用默认属性不设置, 写NULL // 销毁互斥锁 // 当前使用互斥锁的线程退出了, 就可以销毁互斥锁了 int pthread_mutex_destroy(pthread_mutex_t *mutex); // 1. 如果互斥锁状态是打开的, 上锁成功, 锁被锁定 // 2. 如果互斥锁状态已经被锁定了, 线程被阻塞在这把锁上, 当这把锁被打开之后, 线程解除阻塞 int pthread_mutex_lock(pthread_mutex_t *mutex); // 1. 如果互斥锁状态是打开的, 上锁成功, 锁被锁定 // 2. 如果互斥锁状态已经被锁定了,线程不会阻塞在这把锁上, 这个函数不阻塞线程 // 3. 通过返回值判断加锁成功了还是失败了 // 0-> 成功, 非0(错误号) -> 失败 [char* strerror(int errno)] int pthread_mutex_trylock(pthread_mutex_t *mutex); // 打开互斥锁 int pthread_mutex_lock(pthread_mutex_t *mutex); // 关闭互斥锁 int pthread_mutex_unlock(pthread_mutex_t *mutex);
3. 死锁
两个或两个以上的进程在执行过程中,因争夺共享资源而造成的一种互相等待的现象,若无外力作用,它们都将无法推进下去。此时称系统处于死锁状态或系统产生了死锁 。
- 产生死锁场景
- 忘记释放锁,自己将自己锁住
- 当前线程重复申请锁(对一把锁连续加锁两次)
- 多线程多锁申请, 抢占锁资源
- 预防死锁方案
- 多检查
- 使用
trylock
尝试加锁,如果加锁失败,线程不会阻塞 - 访问其他共享资源之前,先释放当前访问资源的锁, 再对其资源加锁
4. 读写锁
-
读写锁类型? 是几把锁?
类型: pthread_rwlock_t
是几把锁? 只有一把锁, 可以锁定读操作, 也可以锁定写操作 -
使用读写锁场景
互斥锁: 不管读写, 都是串行的, 效率低
读写锁: 读: 并行, 写/ 写+读: 串行, 在没有写操作, 单纯读数据的场景下, 效率提升了
程序中对共享资源读操作比较多
的场景下 -
读写锁相关API
#include <pthread.h> // 初始化读写锁 int pthread_rwlock_init(pthread_rwlock_t *restrict rwlock, const pthread_rwlockattr_t *restrict attr); 参数: - rwlock: 需要初始化的读写锁 - attr: 读写锁属性, 使用默认属性, 指定为NULL即可 // 释放读写锁 int pthread_rwlock_destroy(pthread_rwlock_t *rwlock); // 使用读写锁, 锁定读操作 // N个线程都加读读锁, 并且这个锁默认没有锁定, N个线程加读锁成功 // N个线程都加读读锁, 并且这个锁默认被加了读锁, N个线程加读锁成功 // N个线程都加读读锁, 并且这个锁默认被加了写锁, N个线程被阻塞 int pthread_rwlock_rdlock(pthread_rwlock_t *rwlock); // 使用读写锁, 尝试锁定读操作 // N个线程都加读读锁, 并且这个锁默认没有锁定, N个线程加读锁成功 // N个线程都加读读锁, 并且这个锁默认被加了读锁, N个线程加读锁成功 // N个线程都加读读锁, 并且这个锁默认被加了写锁, N个线程不阻塞 int pthread_rwlock_tryrdlock(pthread_rwlock_t *rwlock); // 使用读写锁, 锁定写操作 // 1个线程加写锁, 并且这个锁默认没有锁定, 当前线程加写锁成功 // 1个线程加写锁, 并且这个锁默锁定了写操作, 加锁失败, 当前线程阻塞 // 1个线程加写锁, 并且这个锁默锁定了读操作, 加锁失败, 当前线程阻塞 int pthread_rwlock_wrlock(pthread_rwlock_t *rwlock); // 使用读写锁, 尝试锁定写操作 // 1个线程加写锁, 并且这个锁默认没有锁定, 当前线程加写锁成功 // 1个线程加写锁, 并且这个锁默锁定了写操作, 加锁失败, 不阻塞 // 1个线程加写锁, 并且这个锁默锁定了读操作, 加锁失败, 不阻塞 int pthread_rwlock_trywrlock(pthread_rwlock_t *rwlock); // 对读写锁解锁 int pthread_rwlock_unlock(pthread_rwlock_t *rwlock);
5. 条件变量 ->condition
- 条件变量不是锁,
作用
是阻塞线程/让阻塞的线程解除阻塞- 条件变量需要和互斥锁配合使用
- 互斥锁: 线程同步, 锁共享资源对应的临界区
- 也可以阻塞线程
- 条件变量: 阻塞线程, 阻塞某一部分线程(生产者和消费者模型)
-
条件变量类型
- pthread_cond_t
-
条件变量相关API
#include <pthread.h> int pthread_cond_init(pthread_cond_t *restrict cond,const pthread_condattr_t *restrict attr); 参数: - cond: 要初始化的条件变量 - attr: 条件变量的属性, 默认写NULL // 释放/销毁条件变量 int pthread_cond_destroy(pthread_cond_t *cond); // 线程阻塞函数, 调用这个函数的线程都会被阻塞 int pthread_cond_wait(pthread_cond_t *restrict cond, pthread_mutex_t *restrict mutex); - cond: 初始化的条件变量 - mutex: 互斥锁, 用来做线程同步的 // 表示的时间是从1971.1.1到某个时间点的时间, 总长度使用秒/纳秒表示 struct timespec { time_t tv_sec; /* Seconds */ long tv_nsec; /* Nanoseconds [0 .. 999999999] */ }; // 设置在一个时间段之内阻塞, 超出这个时间之后, 解除阻塞 int pthread_cond_timedwait(pthread_cond_t *restrict cond, pthread_mutex_t *restrict mutex, const struct timespec *restrict abstime); 参数: - cond: 初始化的条件变量 - mutex: 实现线程同步的互斥锁 - abstime: 线程阻塞的时长 // 唤醒阻塞在条件变量上的线程, 至少唤醒一个 int pthread_cond_signal(pthread_cond_t *cond); // 唤醒所有阻塞在条件变量上的线程 int pthread_cond_broadcast(pthread_cond_t *cond);
-
生产者和消费者模型
组成?
- 生产者
- 生成任务, 放到任务队列中
- 程序中需要有若干个线程充当生产者的角色 - 消费者
- 处理任务, 将要处理的任务从任务队列中读出
- 程序中需要有若干个线程充当消费者的角色 - 任务队列 (数据结构中讲的队列)
- 容器
- 多线程操作的共享资源
- 设计到任务队列的相关操作需要做线程同步
- 需要通过条件变量控制生产者和消费者线程工作
- 如果队列为空: 所有消费者线程需要阻塞
- 解除阻塞: 生产者生成了产品, 通过消费者解除阻塞, 继续消费
- 如果队列满了: 所有的生产者线程需要阻塞
- 解除阻塞: 消费者消费了产品, 通知生产者解除阻塞, 继续生产
- 生产者
6. 信号量
信号量用在多线程多任务同步的,一个线程完成了某一个动作就通过信号量告诉别的线程,别的线程再进行某些动作。
信号量不一定是锁定某一个资源,而是流程上的概念,比如:有A,B两个线程,B线程要等A线程完成某一任务以后再进行自己下面的步骤,这个任务 并不一定是锁定某一资源,还可以是进行一些计算或者数据处理之类。
- 信号量主要阻塞线程, 不能完全保证线程安全.
- 如果要保证线程安全, 需要信号量和互斥锁一起使用.
-
信号量类型
sem_t sem;
-
信号量相关API
#include <semaphore.h>
// 信号量初始化
int sem_init(sem_t *sem, int pshared, unsigned int value);
参数:
- sem: 要初始化的信号量
- pshared:
- 0: 线程同步
- 非0: 进程同步
- value: 这个值要设置到第一个参数sem中, 这个值为0, 线程就阻塞了, >0-> 不阻塞
// 销毁/释放信号量
int sem_destroy(sem_t *sem);
// 参数sem中维护一个整形数:
// -- 假设这个整数为0, 调用这个函数的线程阻塞
// -- 假设这个整数>0, 代用这个函数不阻塞, 这整形数 -1
int sem_wait(sem_t *sem);
// 参数sem中维护一个整形数:
// -- 假设这个整数为0, 调用这个函数的线程不阻塞, 返回一个非0值
// -- 假设这个整数>0, 代用这个函数不阻塞, 这整形数 -1
int sem_trywait(sem_t *sem);
// 表示的时间是从1971.1.1到某个时间点的时间, 总长度使用秒/纳秒表示
struct timespec {
time_t tv_sec; /* Seconds */
long tv_nsec; /* Nanoseconds [0 .. 999999999] */
};
// 参数sem中维护一个整形数:
// -- 假设这个整数为0, 调用这个函数的线程阻塞, 阻塞第二个参数指定的时长之后解除阻塞
// -- 假设这个整数>0, 代用这个函数不阻塞, 这整形数 -1
int sem_timedwait(sem_t *sem, const struct timespec *abs_timeout);
// 参数sem中维护一个整形数:
// -- 这整形数 +1
int sem_post(sem_t *sem);
// 打印参数 sem 中的整形数的值当前是多少, 通过第二个参数传出
int sem_getvalue(sem_t *sem, int *sval);