文章目录
- 线程概念
- 线程控制接口和线程id
- 线程优缺点
- 线程互斥和条件变量
- 锁和条件变量相关接口
- POSIX 信号量
- 生产消费者模型
- 阻塞队列实现生产消费者模型
- 环形队列实现生产消费者模型
- 简易懒汉线程池
- 自旋锁和读写锁(了解)
线程概念
在操作系统的的视角下,Linux 下没有真正意义的线程,而是用进程模拟的线程(LWP,轻量级进程),所以 Linux 不会提供直接创建线程的系统调用,最多提供创建轻量级进程的接口。
进程是 CPU 分配资源的基本单位,而线程是 CPU 调度的基本单位,线程的执行粒度比进程更细。一条线程指的是进程中的一条单一顺序的控制流,一个进程中可以并发多个线程,每条线程并行执行不同的任务,各个间共享进程数据,但也拥有自己的一部分数据。
- 1、线程 id
- 2、处理器现场和栈指针(内核栈)
- 3、独立的上下文和栈空间(用户空间栈)
- 4、errno 变量
- 5、信号屏蔽字
- 6、调度优先级
上面我们提到,各个线程之间共享数据,并且线程是用进程模拟的。那么当我们现在想要让不同的线程(执行流),访问进程中一部分资源,我们只需要创建 PCB,并让其指向父进程资源。这种只创建 PCB,并从进程中给它分配资源的执行流就叫做线程。
由上图可以很好地理解线程为什么是 CPU 调度的基本单位,在 CPU 看来,它只关心一个独立的执行流,无论进程内部是一个还是多个执行流,CPU 都是以 task_struct 为单位来调度的。在 CPU 看来,Linux 中的进程比传统中的进程更加轻量化,进程的执行流我们叫轻量化进程。也能很好地理解了为什么进程是分配资源的基本单位,因为进程之间是相互独立的,每个进程都有相应的进程地址空间。
线程共享资源
- 1、文件描述符表
- 2、每种信号的处理方式
- 3、当前工作目录
- 4、用户ID和组ID
- 5、进程地址空间
线程控制接口和线程id
线程在运行的时候我们可以通过 ps -aL
查看线程信息 LWP,即轻量型进程 ID。当 PID==LWP 时,该线程是主线程。当 PID!=LWP 时,该线程是新线程。CPU 调度的时候,是以 LWP 表示特定的一个执行流,之前我们以 PID 识别独立的进程并没有问题,当只有一个执行流的时候,PID 和 LWP 是等价的。
对于用户来说,用户需要的是线程接口。所以 Linux 提供了用户线程库,对下将 Linux 接口封装,对上给用户提供进行线程控制的接口,也就是 pthread 库(原生线程库),这个库不是 Linux 内核的一部分,而是作为用户空间的库提供的。尽管我们包了头文件 <pthread.h>,但这里只有声明,找不到库中对应的方法。对此,在 Linux 下,我们使用线程库时,需要加 -lpthread 选项来表明你要链接的库名称。
线程控制相关接口:
线程创建:int pthread_create(pthread_t *thread, const pthread_attr_t *attr, void *(*start_routine) (void *), void *arg);
第一个参数 pthread_t 是线程 id,第二个参数可以设置线程属性,一般我们设为 nullptr,第三个是一个函数指针,为线程需要的执行方法、任务。void* 让它可以接受任何类型的参数并返回任何类型的结果,类似于 C++ 中的模板,第四个参数为该方法所需要的参数。
线程等待:int pthread_join(pthread_t thread, void **retval);
第一个参数为线程 id,第二个参数是一个二级指针,用来获取,线程退出的返回值。
线程退出: void pthread_exit(void *retval);
线程分离: int pthread_detach(pthread_t thread);
线程取消: int pthread_cancel(pthread_t thread);
这三个函数用法类似,线程退出和取消用来终止线程。线程分离告诉系统,当线程退出时,自动释放线程资源,不必再进行线程等待。
我们可以通过 pthread_t pthread_self(void)
函数来获取线程 id。这里的 id 和前面说的线程ID(LWP) 不是一回事。前面讲的线程ID属于进程调度的范畴。因为线程是轻量级进程,是操作系统调度器的最小单位,所以需要一个数值来唯一表示该线程。pthread_t 这个线程 id 指向一个虚拟内存单元,本质就是进程地址空间上的一个地址。线程库的后续操作,就是根据该线程 id 来操作线程的。
描述用户级线程的结构体是在用户空间线程库中维护的,即 pthread 库。其中第一个字段 struct pthread 包括了线程的属性。第二个字段 线程局部存储 用于保存用 __thread 修饰的全局变量,让每个线程独有该变量。主线程的栈正常保存在地址空间的栈中,其他线程的独立栈,都在该结构体第三个字段中,用来保存本线程产生的临时数据。
线程优缺点
优点:
- 1、创建一个新线程的代价要比创建一个新进程小得多,线程占用的资源要比进程少很多
- 2、进程间切换,需要切换页表、虚拟空间、切换PCB、切换上下文
- 3、线程间切换,线程都指向同一个地址空间,页表和虚拟地址空间就不需要再进行切换了,只需要切换PCB和上下文,成本较低
- 4、线程切换不需要更新太多cache(缓存大量经常使用的数据),进程切换要全部更新
缺点:
- 1、一个线程异常,整个进程也会随之崩溃
- 2、竞态条件:当多个线程同时访问和修改共享数据时,可能会发生竞态条件,导致数据不一致或错误
- 3、编程难度提高:编写与调试一个多线程程序比单线程程序困难得多
线程互斥和条件变量
线程互斥相关概念:
- 临界资源:任何一个时刻,都只允许一个执行流进行访问的共享资源
- 临界区:每个线程内部,访问临界资源的代码,就叫做临界区
- 互斥:任何时刻,互斥保证有且只有一个执行流进入临界区,访问临界资源,通常对临界资源起保护作用
- 同步:在保证数据安全的前提下,让线程能够按照某种特定的顺序访问临界资源,从而有效避免饥饿问 题
- 原子性:不会被任何调度机制打断的操作,该操作只有两态,要么完成,要么未完成
由上面线程缺点我们知道,多线程并发访问时,会出现竞态条件,导致数据不一致或错误问题。对此,我们需要对临界资源进行保护,用互斥保证每次有且只有一个执行流进入临界区,用同步让线程访问资源具有一定的顺序性!
锁是实现线程互斥的基本同步机制,确保在多线程环境中,任一时刻仅有一个线程能够访问共享资源。这种机制通过阻止其他线程访问已被锁定的资源,直到持有锁的线程释放它,从而允许其他线程获取锁并进行操作。锁的实现可以通过编程语言提供的库或操作系统的API,例如 Linux 中的互斥锁(mutex),来确保线程安全地管理对共享资源的访问。
Linux 中的条件变量提供了一种高效的同步机制,允许多个线程在某个特定条件未满足时安全地挂起执行,直到被其他线程触发或条件成立。这种机制避免了不必要的线程轮询和忙等待,从而优化了系统性能。
锁和条件变量相关接口
锁 :pthread_mutex_t
初始化锁:int pthread_mutex_init(pthread_mutex_t *restrict mutex, const pthread_mutexattr_t *restrict attr);
第一个参数传锁的地址,第二个参数设置锁的属性,暂时设为 nullptr。也可以通过下面的方式对全局锁进行初始化,同时全局的锁初始化后,不需要手动销毁:pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
阻塞式申请锁:int pthread_mutex_lock(pthread_mutex_t *mutex);
非阻塞式申请锁:int pthread_mutex_trylock(pthread_mutex_t *mutex);
解锁:int pthread_mutex_unlock(pthread_mutex_t *mutex);
锁的销毁:int pthread_mutex_destroy(pthread_mutex_t *mutex);
pthread_mutex_lock 申请锁时,如果锁已经被其它线程申请了,就会阻塞等待。而 pthread_mutex_trylock 申请锁时,如果没有申请到锁时,会立即返回,而不是进行阻塞等待。申请成功返回0,失败返回错误码。
加锁的本质是对被加锁的代码区域,让多线程进行串行访问,对此我们应该尽量让临界区代码越少越好。 同时不要销毁一个已经加锁的互斥量,避免后面有其它线程再尝试加锁时,申请不到锁,而产生死锁问题:在一组进程中,其中每个进程都持有一些资源,并且等待其他进程释放它们所占用的资源。由于每个进程都在等待其他进程先释放资源,这导致所有进程都无法继续执行,从而陷入一种永久的等待状态。破坏下面四个条件中的任意一个就能解决死锁问题
死锁四个必要条件:
- 互斥条件:一个资源每次只能被一个执行流使用
- 请求与保持条件:一个执行流因请求资源而阻塞时,对已获得的资源保持不放
- 不剥夺条件:一个执行流已获得的资源,在末使用完之前,不能强行剥夺
- 循环等待条件:若干执行流之间形成一种头尾相接的循环等待资源的关系
条件变量:pthread_cond_t
初始化条件变量: int pthread_cond_init(pthread_cond_t *restrict cond, const pthread_condattr_t *restrict attr);
这个函数其初始化方式和锁类似,第一个参数为条件变量地址,第二个参数设置条件变量属性。也可以直接初始化一个全局的条件变量,不需要手动销毁: pthread_cond_t cond = PTHREAD_COND_INITIALIZER;
等待:int pthread_cond_wait(pthread_cond_t *restrict cond,pthread_mutex_t *restrict mutex);
条件变量可以简单抽象为一个阻塞队列,等待特定条件的线程会放入到队列中,合适的时候,通过自己的唤醒机制唤醒它们。特别注意:进入等待的线程会释放掉身上的锁,避免其它线程出现永远拿不到锁而产生的死锁问题。当线程被唤醒时会重新持有锁,执行后面的临界区代码
唤醒一个等待线程:int pthread_cond_signal(pthread_cond_t *cond);
唤醒所有等待线程:int pthread_cond_broadcast(pthread_cond_t *cond);
销毁条件变量: int pthread_cond_destroy(pthread_cond_t *cond);
POSIX 信号量
当我们仅用一个互斥锁对临界资源进行保护时,相当于我们将这块临界资源看作一个整体,同一时刻只允许一个执行流对这块临界资源进行访问。但其实我们可以将这块临界资源再分割为多个区域,当多个执行流需要访问临界资源时,那么我们可以让这些执行流同时访问临界资源的不同区域,此时不会出现数据不一致问题。而不同区域资源数量的多少我们可以用信号量来保证,信号量的本质是保证PV操作,具有原子性的一把计数器。
P操作: 申请信号量,申请信号量的本质就是申请获得临界资源中某块资源的使用权限,当申请成功时临界资源中资源的数目应该减去一.如果它的值为零,表明没有可以申请的资源了,就挂起该进程。
V操作: 释放信号量,释放信号量的本质就是归还临界资源中某块资源的使用权限,当释放成功时,临界资源中资源的数目就应该加一。PV操作自己已经保证了原子性,不需要我们再加以保护。
初始化信号量:int sem_init(sem_t *sem, int pshared, unsigned int value);
sem:需要初始化的信号量。pshared:零值表示线程间共享,传入非零值表示进程间共享。value:设置信号量计数器的初始值。剩下的函数使用方式相同,参数都是信号量。
P操作:int sem_wait(sem_t *sem);
V操作:int sem_post(sem_t *sem);
销毁信号量:int sem_destroy(sem_t *sem);
生产消费者模型
生产者消费者模型就是通过一个容器来解决生产者和消费者的强耦合问题,两者之间没有直接的关系,不直接进行通讯,而是通过容器来进行通讯。所以生产者生产完数据之后,直接将生产的数据放到这个容器当中,消费者直接从这个容器里取数据,这个容器就相当于一个缓冲区。这个容器就是一个交易场所,用来给生产者和消费者解耦的。
模型满足以下简单321原则:
- 1、三种关系: 生产者和生产者(互斥关系)、消费者和消费者(互斥关系)、生产者和消费者(互斥关系和同步关系)
- 2、两种角色: 生产者和消费者。(通常由线程承担)
- 3、一个交易场所: 通常指的是内存中的一段缓冲区
而从编码的角度,每一个消费者或者生产者我们都可以看作一个线程。所有的消费者和生产者都要访问同一个交易场所,于是交易场所就是我们之前谈到的临界资源!显然,它需要一定的互斥和同步进行保护。生产者和生产者、消费者和消费者、生产者和消费者之间也具有明显的互斥关系:因为这里的交易场所是临界资源(目前视作一个整体),一次只能让一个执行流进入。而生产者和消费者之间还存在同步关系:必须先生产才能消费,交易场所空了就不能消费了。同时交易场所是有容量的,当生产满了时必须停下来,让消费者来消费。
阻塞队列实现生产消费者模型
我们把阻塞队列这个临界资源被当成了一个大的整体,因此只需要一把锁,维持消费者和生产者之间的互斥关系。当阻塞队列为空或者为满时,需要条件变量来维持生产者和消费者之间的同步关系。 生产者线程之间的互斥竞争关系以及消费者线程之间的互斥竞争关系,只需要上面那把锁便能维护。但是为了避免多线程竞争时的饥饿问题,我们用两个条件变量分别让生产者线程和消费者线程在各自的队列中等待,这样就实现的生产者线程之间的同步,也实现了消费者线程之间的同步。
由于阻塞队列中这个临界资源被当成了一个大的整体,一次只能让一个线程进来生产或者消费。那么它们之间共同访问临界区时,是互斥串行访问的,并不能体现生产消费者模型的高效性。而它的高效性,恰恰体现在它们的非临界区(准备数据、加工数据)。生产者的临界区代码(生产时)和消费者的非临界区代码(取走数据后,进行加工处理)进行交叉时,是可以并发进行访问的。同样生产者的非临界区代码(获取数据到生产前)和消费者的临界区代码进行交叉时(取走数据时)也是可以并发进行访问的,这才是高效性的体现。核心实现:
BlockQueue.hpp
#include <iostream> #include <unistd.h> #include <queue> #include <pthread.h> using namespace ::std; template <class T> class BlockQueue { public: BlockQueue(int capacity = 20) : _max_size(capacity) { _low_line = _max_size / 3; _high_line = _max_size * 2 / 3; pthread_mutex_init(&_mutex, nullptr); pthread_cond_init(&_p_cond, nullptr); pthread_cond_init(&_c_cond, nullptr); } T Pop() { pthread_mutex_lock(&_mutex); // 加锁一定加载判断之前 // if (_q.size() == 0) // 消费完了,进入条件变量中等待 // 下面这里用while判断而不用if,可以防止多线程下误唤醒溢出的情况 while (_q.size() == 0) // 消费完了,进入条件变量中等待 { pthread_cond_wait(&_c_cond, &_mutex); // 释放锁,阻塞等待ing,唤醒时重新持有锁 } T out = _q.front(); _q.pop(); if (_q.size() < _low_line) pthread_cond_signal(&_p_cond); // 唤醒生产者线程 pthread_mutex_unlock(&_mutex); return out; } void Push(const T &in) { pthread_mutex_lock(&_mutex); // 加锁一定加载判断之前 // if (_q.size() == _max_size) // 生产满了,进入条件变量中等待 // 下面这里用while判断而不用if,防止多线程下伪唤醒溢出的情况 while (_q.size() == _max_size) // 生产满了,进入条件变量中等待 { pthread_cond_wait(&_p_cond, &_mutex); // 释放锁,阻塞等待ing,唤醒时重新持有锁 } _q.push(in); if (_q.size() > _high_line) pthread_cond_signal(&_c_cond); // 唤醒消费者线程 pthread_mutex_unlock(&_mutex); } size_t Size() { return _q.size(); } ~BlockQueue() { pthread_mutex_destroy(&_mutex); pthread_cond_destroy(&_p_cond); pthread_cond_destroy(&_c_cond); } private: queue<T> _q; int _max_size; pthread_mutex_t _mutex; // 锁来保证互斥 pthread_cond_t _p_cond; // 条件变量来保证同步 pthread_cond_t _c_cond; // 用两个队列保证多线程生产和多线程消费各自同步 int _low_line; // 低水位线 int _high_line; // 高水位线 };
环形队列实现生产消费者模型
我们把环形队列这个大的临界资源分为两个区域,一个是可生产的区域(即剩余的,可用来生产的空间),另外一个是可消费的区域(已经生产的,可用来消费的空间),两个区域资源数量的多少,用两个信号量来表示。
结合上图来说,非空非满时,生产者使用的是P下标来进行生产,而消费者使用的是C下标来进行消费,它们在各自的临界区中运行互不影响,所以能并发进行访问。当PC下标重合时,表示为环形队列为空或者为满,意味着有一个临界区的信号量为0,这时整个环形队列又变成了一个大的临界资源。因为只有一个临界资源,当然只能让一个线程进入到环形队列里生产或者消费,表现出生产者和消费者之间的局部互斥性。为空的时候,一定要让生产者先运行。为满的时候,一定让消费者先运行,体现出它们之间的局部同步性。生产者和消费者之间的互斥,同步关系已经由信号量进行承担了。而生产者与生产者之间、消费者与消费者之间的互斥竞争关系,则需要由锁来维护,有两个临界资源,自然需要两把锁。
当线程进入到环形队列的时候,它既要申请信号量,又要申请锁,那么先申请哪个呢?如果先申请锁,再申请信号量,它们是串行的:在锁被使用的情况下,只能阻塞等待锁被释放,才能去申请信号量。而先申请信号量,再申请锁,即使锁在被占用的情况下,各个线程可以先并发去申请信号量,再来锁这排队。同时,信号量的申请和释放是原子的,不需要加锁保护。秉持临界区代码越少越好原则,顺序为:申请信号量在加锁之前,释放信号量在解锁之后。
核心实现:
RingQueue.hpp#pragma once #include <vector> #include <pthread.h> #include <semaphore.h> template <class T> class RingQueue { private: void P(sem_t& sem) { sem_wait(&sem); } void V(sem_t& sem) { sem_post(&sem); } void Lock(pthread_mutex_t& mutex) { pthread_mutex_lock(&mutex); } void UnLock(pthread_mutex_t& mutex) { pthread_mutex_unlock(&mutex); } public: RingQueue(int size=10) :_q(size),_size(size) { sem_init(&_p_sem,0,size); sem_init(&_c_sem,0,0); pthread_mutex_init(&_p_mutex, nullptr); pthread_mutex_init(&_c_mutex, nullptr); } void Push(const T& in) { P(_p_sem); // 先申请自己的信号量 Lock(_p_mutex); // 加锁在后 _q[_p_index]=in; _p_index++; _p_index%=_size; //保持环形特征 UnLock(_p_mutex); V(_c_sem); // 释放的信号量 } T Pop() { P(_c_sem); // 先申请自己的信号量 Lock(_c_mutex); // 加锁在后 T out=_q[_c_index]; _c_index++; _c_index%=_size; //保持环形特征 UnLock(_c_mutex); V(_p_sem); // 释放的信号量 return out; } ~RingQueue() { pthread_mutex_destroy(&_p_mutex); pthread_mutex_destroy(&_c_mutex); sem_destroy(&_p_sem); sem_destroy(&_c_sem); } private: std::vector<T> _q; // 环形队列 int _size; // 容量 int _p_index=0; // 生产者下标 int _c_index=0; // 消费者下标 sem_t _p_sem; // 可生产的信号量 sem_t _c_sem; // 可消费的信号量 pthread_mutex_t _p_mutex; // 多生产者之间竞争的锁 pthread_mutex_t _c_mutex; // 多消费者之间竞争的锁 };
简易懒汉线程池
线程池模型本质上是生产消费者模型的一种应用,它通过预先创建一批线程来准备处理任务,这些线程可以视为消费者,而任务队列则充当了存储任务的缓冲区。当有新任务到来时,它们被推送到任务队列中,等待线程池中的线程来处理。休眠的线程在检测到队列中有任务时会被唤醒,然后开始执行任务。 线程池具有应对突发性大量任务请求的能力,当大量任务请求到来时,无需再开销创建线程,直接让休眠的线程去处理任务,同时还能复用已经处理完任务的线程,让它继续处理下一个任务。
线程池的优点包括:
- 提高性能:线程池可以减少线程创建和销毁的开销,避免了频繁地创建和销毁线程的系统开销。通过重用线程,可以更有效地利用系统资源,提高系统的整体性能。
- 提高响应速度:线程池可以减少任务启动的延迟时间。当有新任务到达时,线程池中的线程可以立即执行任务,而不需要等待新线程的创建。
- 控制并发数量:线程池可以限制并发执行的线程数量。通过设置最大线程数,可以避免系统因为过多的线程而导致资源消耗过大或者负载过高的问题。
- 简化线程管理:线程池封装了线程的创建、销毁和管理等复杂的操作。开发者只需要提交任务给线程池,不需要手动管理线程的生命周期,简化了线程的编程模型。
- 提供任务队列:线程池通常提供一个任务队列来存储待执行的任务。如果当前线程池中的线程已满,新任务会被放入队列中等待执行,从而避免任务丢失。
核心实现:
ThreadPool.hpp#pragma once #include <vector> #include <pthread.h> #include <thread> #include <queue> #include <unordered_map> #include <string> #include "Task.hpp" using namespace::std; template <class T> class ThreadPool { public: void Lock() { pthread_mutex_lock(&_mutex); } void UnLock() { pthread_mutex_unlock(&_mutex); } void Wait() { pthread_cond_wait(&_cond,&_mutex); } void WakeUp() { pthread_cond_signal(&_cond); } bool IsQueueEmpty() { return _tasks.empty(); } public: static ThreadPool<T> *GetInstance(int size=5) { // 避免每次进来都加锁再判断,因为只有第一次获取单例才有并发问题 if (_tp==nullptr) { pthread_mutex_lock(&Singleton_Lock); if (_tp==nullptr) { cout << "log: singleton create done first !!!" << endl; _tp= new ThreadPool<T>(size); } pthread_mutex_unlock(&Singleton_Lock); } return _tp; } void Start() { for(int i=0;i<_tds.size();i++) { _tds[i]=thread(func,this); _hash[_tds[i].get_id()]="thread_"+to_string(i); } } static void *func(void* args) { ThreadPool<T> *tp=static_cast<ThreadPool*>(args); // 线程处理任务 while(true) { Task t=tp->Pop(); t(); cout <<tp->_hash[(thread::id)pthread_self()]<<" consume a task, task is : " << t.GetTask() << " result: " << t.GetResult() << endl; } } void Push(const T& t) { Lock(); _tasks.push(t); UnLock(); WakeUp(); } Task Pop() { Lock(); while(IsQueueEmpty()) { Wait(); } T t =_tasks.front(); _tasks.pop(); UnLock(); return t; } private: ThreadPool(int size=5) :_tds(size) { pthread_mutex_init(&_mutex, nullptr); pthread_cond_init(&_cond,nullptr); } ~ThreadPool() { pthread_mutex_destroy(&_mutex); pthread_cond_destroy(&_cond); } ThreadPool(const ThreadPool<T>&)=delete; const ThreadPool<T>& operator=(const ThreadPool<T>&)=delete; private: vector<thread> _tds; unordered_map<thread::id,string> _hash; queue<T> _tasks; // 任务队列 pthread_mutex_t _mutex; pthread_cond_t _cond; // 单例 static ThreadPool<T>* _tp; static pthread_mutex_t Singleton_Lock; }; template <class T> ThreadPool<T>* ThreadPool<T>::_tp=nullptr; template <class T> pthread_mutex_t ThreadPool<T>::Singleton_Lock=PTHREAD_MUTEX_INITIALIZER;
自旋锁和读写锁(了解)
上面我们学到的互斥锁,信号量这些,一旦申请失败,线程就会被阻塞挂起,我们称这样的锁为挂起等待锁(悲观锁)。其它线程访问临界区时间较长,那么线程需要等待的时间就长,挂起等待是合适的。而如果其它线程访问临界区时间较短,我们就可以使用自旋锁,自旋锁如果申请失败,线程并不会挂起等待,它会选择自旋继续申请。它就像是一直循环的
int pthread_mutex_trylock(pthread_mutex_t *mutex);
,直到申请成功。
自旋锁:pthread_spinlock_t
int pthread_spin_destroy(pthread_spinlock_t *lock);
int pthread_spin_init(pthread_spinlock_t *lock, int pshared);
int pthread_spin_lock(pthread_spinlock_t *lock);
int pthread_spin_trylock(pthread_spinlock_t *lock);
int pthread_spin_unlock(pthread_spinlock_t *lock);
读写锁:
pthread_rwlock_t
读者写者模型也满足321原则,不过这里读者并没有把数据取走,因此读者与读者之间没有互斥关系。写者与写者之间保持互斥竞争关系,读者与写者之间为互斥同步关系,避免没写完,读取时的数据不完整问题。
int pthread_rwlock_init(pthread_rwlock_t *restrict rwlock,const pthread_rwlockattr_t *restrict attr);
int pthread_rwlock_destroy(pthread_rwlock_t *rwlock);
int pthread_rwlock_unlock(pthread_rwlock_t *rwlock);
int pthread_rwlock_rdlock(pthread_rwlock_t *rwlock);
int pthread_rwlock_tryrdlock(pthread_rwlock_t *rwlock);
int pthread_rwlock_wrlock(pthread_rwlock_t *rwlock);
int pthread_rwlock_trywrlock(pthread_rwlock_t *rwlock);
设置读者写者的优先问题:int pthread_rwlockattr_setkind_np(pthread_rwlockattr_t *attr, int pref);
pref 共有 3 种选择:
PTHREAD_RWLOCK_PREFER_READER_NP: (默认设置) 读者优先,可能会导致写者饥饿情况
PTHREAD_RWLOCK_PREFER_WRITER_NP: 写者优先,目前有 BUG,可能会导致读者饥饿情况
PTHREAD_RWLOCK_PREFER_WRITER_NONRECURSIVE_NP: 写者优先,但写者不能递归加锁