目录
概念
- 线程安全:描述的是进程中的线程对临界资源的访问操作是否是安全的;
- 临界资源:多线程执行流共享的资源就叫做临界资源;
- 临界区:每个线程内部,访问临界资源的代码,就叫做临界区;
- 原子性(后面讨论如何实现):不会被任何调度机制打断的操作,该操作只有两种状态,要么完成,要么未完成;
- 同步:通过条件判断使对临界资源访问或获取更加合理;
- 互斥:通过对临界资源同一时间的唯一访问保证访问操作的安全;
线程互斥
抢票代码举例
- 先看一个例子:
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <pthread.h>
//先设置票数总共有100张
int tickets = 100;
//线程的回调函数
void* scalpers(void* arg){
//循环抢票
while(1) {
//判断票数是否存在,如果有票,则抢票
if (tickets > 0) {
usleep(5);
tickets--;
printf("抢到票,还剩:%d 张\n", tickets);
}else {
//没票了则退出
pthread_exit(NULL);
}
}
return NULL;
}
int main (int argc, char* argv[]){
//创建四个线程去执行抢票流程
pthread_t tid[4];
int ret;
for(int i = 0; i < 4; i++) {
ret = pthread_create(&tid[i], NULL, scalpers, NULL);
//创建失败则退出
if (ret != 0) {
printf("thread create error\n");
return -1;
}
}
//等待四个线程退出
for (int i = 0; i < 4; i++) {
pthread_join(tid[i], NULL);
}
return 0;
}
- 错误:该代码是一个抢票流程,我们执行上述代码,在票数还有一张的时候,如果有多个线程都去进行抢票,那么最后出来的票数剩余结果会是一个负数,出现错误;
- 原因:
if
语句判断条件为真以后,利用usleep
来模拟漫长业务的过程,在这个漫长的业务过程中,可能有很多个线程会进入该代码段,这样就导致--ticket
操作本身就不是一个原子操作; - 解决:要想解决这个问题,本质上就是需要一把锁,在有线程进行操作时,就将其锁上,使得其他线程不能进入操作,在 Linux 中提供的这把锁叫互斥量。
- 疑问:如何保证设置互斥量的这个操作时互斥的呢,如果无法保证,那么就不能实现线程之间的互斥;
互斥量概念
- 本质:是一个 0/1 计数器,用于标记临界资源的访问状态,其中:0——不可访问,1——可访问;
- 原理:在访问临界资源之前:判断是否可访问,可访问则加锁置为不可访问,若不可访问则阻塞;在访问临界资源完毕之后解锁,将资源状态置为可访问;
互斥量操作
- 定义互斥量
pthread_mutex_t mutex;
- 初始化互斥量:有以下两种方法进行初始化
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER
:静态分配,在定义时使用系统提供的宏来赋值;int pthread_mutex_init(pthread_mutex_t* mutex, const pthread_mutexattr_t* attr);
:动态分配;mutex
:要初始化的互斥量;attr
:设置互斥量的属性,不过一般没有什么要设置的,所以通常置为 NULL;
- 在访问临界资源之前加锁:有以下两种方式进行加锁
int pthread_mutex_lock(pthread_mutex_t* mutex);
:对互斥量进行加锁,如果不能加锁,那么则阻塞等待;int pthread_mutex_trylock(pthread_mutex_t* mutex);
:对互斥量进行加锁,如果不能加锁,那么则立即返回错误编号,如果返回值为 EBUSY,则说明锁被别人加了,此时需要进行循环加锁;
- 在访问临界资源之后解锁:在任何有可能退出的位置都要进行解锁,否则我们再次访问互斥量时是加锁状态,那么就修改不了,使用不了互斥量了;
int pthread_mutex_unlock(pthread_mutex_t* mutex);
访问完临界资源之后,对互斥量进行解锁;
- 销毁互斥量
int pthread_mutex_destory(pthread_mutex_t* mutex);
不再需要互斥量时,销毁互斥量;
抢票代码改进
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <pthread.h>
//先设置票数总共有100张
int tickets = 100;
//线程的回调函数
void* scalpers(void* arg){
//先拿到互斥量,此处拿到的是互斥量的地址
pthread_mutex_t* mutex = (pthread_mutex_t*)arg;
//循环抢票
while(1) {
//加锁
pthread_mutex_lock(mutex);
//判断票数是否存在,如果有票,则抢票
if (tickets > 0) {
usleep(5);
tickets--;
printf("抢到票,还剩:%d 张\n", tickets);
}else {
//没票了则退出,在退出前先解锁
pthread_mutex_unlock(mutex);
pthread_exit(NULL);
}
//抢完票之后解锁
pthread_mutex_unlock(mutex);
}
return NULL;
}
int main (int argc, char *argv[]){
//设置互斥量
pthread_mutex_t mutex;
//初始化互斥量
pthread_mutex_init(&mutex, NULL);
//创建四个线程去执行抢票流程
pthread_t tid[4];
int ret;
for(int i = 0; i < 4; i++) {
ret = pthread_create(&tid[i], NULL, scalpers, (void*)&mutex);
//创建失败则退出
if (ret != 0) {
printf("thread create error\n");
return -1;
}
}
//等待四个线程退出
for (int i = 0; i < 4; i++) {
pthread_join(tid[i], NULL);
}
//销毁互斥量
pthread_mutex_destroy(&mutex);
return 0;
}
死锁
概念
- 概念:死锁是指在一组进程中的各个线程均占有不会释放的资源,但因互相申请被其他线程程所占用的不会释放的资源而处于的一种永久等待状态;
产生的四个必要条件
- 互斥条件:一个资源同一时间只有一个进程 / 线程能够访问;
- 不可剥夺条件:只能由同一个线程来完成加锁与解锁的过程;
- 请求与保持:对 A 资源进行加锁后,请求 B 资源,但是未能请求到,那么不会释放 A 资源;
- 环路等待条件:线程 1 对 A 资源加了锁,此时请求 B 资源,线程 2 对 B 资源加了锁,此时请求 A 资源,那么这两个线程就会形成环路请求,谁也不会让着谁;
预防死锁
- 产生死锁的前两个条件是互斥锁自身的性质,所以不能破坏,因此只能破坏产生死锁的 3、4 条件;
- 保证加锁与解锁顺序一致;
- 请求不到第二个资源则释放当前已有资源;
- 对线程所需资源进行一次性分配;
避免死锁
线程同步
条件变量
- 概念:通过条件判断实现对资源获取的合理性;
- 应用:当一个线程互斥地访问某个变量时,它可能发现在其它线程改变状态之前,它什么也做不了;例如一个线程访问队列时,发现队列为空,它只能等待,直到其它线程将一个节点添加到队列中,这种情况就需要用到条件变量;
- 本质:pcb 等待队列 + 能够阻塞线程以及唤醒线程的接口;
注意:
- 条件变量实现同步这里,虽然是拥有等待队列的,但是只是提供了阻塞线程以及唤醒线程的接口,至于什么时候阻塞、什么时候唤醒需要程序员自己判断;
- 条件变量使用中,对条件判断由程序员自己完成,而条件判断的依据是一个临界资源,访问时需要被保护,因此条件变量需要搭配互斥量一起使用;
条件变量操作
- 定义条件变量
pthread_cond_t cond;
:定义一个条件变量;
- 初始化条件变量:有以下两种方法:
pthread_cond_t cond = PTHREAD_COND_INITIALIZER;
:在定义时对条件变量进行赋值;int pthread_cond_init(pthread_cond_t* cond, pthread_condattr_t* attr);
cond
:要初始化的条件变量;attr
:设置条件变量的属性,不过一般没有什么要设置的,所以通常置为 NULL;
- 使线程阻塞:如果拿不到自己所需的资源,那么就先阻塞等待;
int pthread_cond_wait(pthread_cond_t* cond, pthread_mutex_t* mutex);
:需要资源的线程在休眠之前先对互斥量解锁,否则产生资源的线程拿不到判断条件(互斥量),等资源产生之后再唤醒被阻塞线程,唤醒之后再对互斥量进行加锁;cond
:条件变量;mutex
:互斥量;
int pthread_cond_timedwait(pthread_cond_t* cond, pthread_mutex_t* mutex, struct timespec* abstime);
:限制阻塞时长的阻塞等待,在一定时间内进行等待,时间一到就报错返回;
- 唤醒阻塞线程
int pthread_cond_signal(pthread_cond_t* cond);
:唤醒至少一个被阻塞的线程;int pthread_cond_broadcast(pthread_cond_t* cond);
:唤醒全部被阻塞的线程;
- 销毁条件变量
int pthread_cond_destroy(pthread_cond_t* cond);
:销毁指定条件变量;
一个厨师与一个顾客的例子
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <pthread.h>
//设置一个互斥量和一个条件变量
pthread_mutex_t mutex;
pthread_cond_t cond;
int bowl = 1;//代表了一个碗
//厨师的执行流程
void *cooker(void *arg){
while(1) {
//加锁
pthread_mutex_lock(&mutex);
//判断碗中是否有饭,如果有饭则需阻塞等待被唤醒
if(bowl == 1){
pthread_cond_wait(&cond, &mutex);
}
//如果没有饭则做饭
printf("我做了一碗美味的饭~~\n");
bowl++;
//做完饭之后唤醒等待队列中的其他线程,由于只有一个厨师和一个顾客,所以唤醒的是顾客
pthread_cond_signal(&cond);
//解锁
pthread_mutex_unlock(&mutex);
}
return NULL;
}
//顾客的执行流程
void *customer(void *arg){
while(1) {
//加锁
pthread_mutex_lock(&mutex);
//判断碗中是否有饭,如果没有饭则需阻塞等待被唤醒
if(bowl == 0){
pthread_cond_wait(&cond, &mutex);
}
//如果有饭则吃饭
printf("真好吃~\n");
bowl--;
//吃完饭之后唤醒等待队列中的其他线程,由于只有一个厨师和一个顾客,所以唤醒的是厨师
pthread_cond_signal(&cond);
//解锁
pthread_mutex_unlock(&mutex);
}
return NULL;
}
int main (int argc, char *argv[]){
pthread_t ctid, dtid;
int ret;
//初始化互斥量和条件变量
pthread_mutex_init(&mutex, NULL);
pthread_cond_init(&cond, NULL);
//创建厨师线程
ret = pthread_create(&ctid, NULL, cooker, NULL);
if (ret != 0) {
printf("thread create error\n");
return -1;
}
//创建顾客线程
ret = pthread_create(&dtid, NULL, customer, NULL);
if (ret != 0) {
printf("thread create error\n");
return -1;
}
//等待线程的退出,不关心返回值,所以第二个参数设为NULL
pthread_join(ctid, NULL);
pthread_join(dtid, NULL);
//销毁互斥量和条件变量
pthread_mutex_destroy(&mutex);
pthread_cond_destroy(&cond);
return 0;
}
多个厨师与多个顾客的例子
- 说明:当执行流程不再是一个顾客与一个厨师时,而是变成多个顾客与多个厨师时,那么就会产生以下两个问题:
- 顾客线程出现问题则会导致一直吃饭的情况,厨师线程出问题则会导致一直做饭的情况,线面就以顾客线程出问题来书明(厨师线程出问题与这类似):
- 说明:假设此时碗中无饭,当多个顾客进行吃饭,第一个顾客先抢到锁,进行加锁,然后判断碗中无饭,所以解锁进入休眠状态,其余顾客类似,所以多个顾客都走到了休眠状态,此时厨师做了一碗饭,然后唤醒等待队列中的线程,由于
signal
函数是唤醒至少一个等待线程,因此多个顾客都被唤醒;
此时多个顾客被唤醒后需要再次加锁,第一个顾客先抢到锁,进行加锁,然后吃饭,吃完唤醒等待队列线程,然后进行解锁,此时本来应该由厨师拿到锁进行做饭,但是如果被顾客拿到了,那么就会在碗中没有饭的情况下吃饭,吃完唤醒等待队列线程,然后解锁;
如果每次在顾客吃完饭并唤醒等待队列线程之后,都是由没拿到锁而等待的顾客拿到锁,那么就会出现即使碗中无饭,也会一直吃饭的情况;(厨师的问题也是如此) - 解决:处于休眠状态的线程在被唤醒之后再次进行条件变量的判断,也就是说在判断碗中是否有饭的地方进行循环判断,被唤醒之后就进行判断,如果条件满足则向下走,否则继续阻塞等待;
- 代码:
- 说明:假设此时碗中无饭,当多个顾客进行吃饭,第一个顾客先抢到锁,进行加锁,然后判断碗中无饭,所以解锁进入休眠状态,其余顾客类似,所以多个顾客都走到了休眠状态,此时厨师做了一碗饭,然后唤醒等待队列中的线程,由于
//循环判断碗中是否有饭,如果有饭则需阻塞等待被唤醒
while(bowl == 1){
pthread_cond_wait(&cond, &mutex);
}
//循环判断碗中是否有饭,如果没有饭则需阻塞等待被唤醒
while(bowl == 0){
pthread_cond_wait(&cond, &mutex);
}
- 当我们做了如上改进之后还是会出现问题,会引发死锁问题:
- 说明:其实还是上面的流程导致的,当一位顾客吃完饭之后,进行唤醒等待队列线程的操作,由于队列中有许多线程,所以排在前面的并不一定是厨师,如果排在等待队列前面的是许多顾客线程,那么会出现只唤醒了一部分的顾客而没有唤醒厨师的情况,这将会导致这些被唤醒的顾客就会全都陷入休眠状态,没有线程会去唤醒厨师了,最终导致死锁;
- 解决:对每一个角色都创建一个条件变量,这样每个角色都会有自己的等待队列,我们进行唤醒时,只需唤醒对应的等待队列即可;
- 代码:
#include <pthread.h>
//设置一个互斥量和两个条件变量
pthread_mutex_t mutex;
pthread_cond_t cond_customer;
pthread_cond_t cond_cooker;
int bowl = 1;//代表了一个碗
//厨师的执行流程
void *cooker(void *arg){
while(1) {
//加锁
pthread_mutex_lock(&mutex);
//循环判断碗中是否有饭,如果有饭则需阻塞等待被唤醒,并且等待在对应的队列中
while(bowl == 1){
pthread_cond_wait(&cond_cooker, &mutex);
}
//如果没有饭则做饭
printf("我做了一碗美味的饭~~\n");
bowl++;
//做完饭之后唤醒另外一个队列中的线程,这样就不会出问题
pthread_cond_signal(&cond_customer);
//解锁
pthread_mutex_unlock(&mutex);
}
return NULL;
}
//顾客的执行流程
void *customer(void *arg){
while(1) {
//加锁
pthread_mutex_lock(&mutex);
//循环判断碗中是否有饭,如果没有饭则需阻塞等待被唤醒,并且等待在对应的队列中
while(bowl == 0){
pthread_cond_wait(&cond_customer, &mutex);
}
//如果有饭则吃饭
printf("真好吃~\n");
bowl--;
//吃完饭之后唤醒对应等待队列中的线程,这样就不会出现问题
pthread_cond_signal(&cond_cooker);
//解锁
pthread_mutex_unlock(&mutex);
}
return NULL;
}
int main (int argc, char *argv[]){
pthread_t ctid[4], dtid[4];
int ret;
//初始化互斥量和条件变量
pthread_mutex_init(&mutex, NULL);
pthread_cond_init(&cond_cooker, NULL);
pthread_cond_init(&cond_customer, NULL);
//创建厨师线程
for (int i = 0; i < 4; i++) {
ret = pthread_create(&ctid[i], NULL, cooker, NULL);
if (ret != 0) {
printf("thread create error\n");
return -1;
}
}
//创建顾客线程
for (int i = 0; i < 4; i++) {
ret = pthread_create(&dtid[i], NULL, customer, NULL);
if (ret != 0) {
printf("thread create error\n");
return -1;
}
}
//等待线程的退出,不关心返回值,所以第二个参数设为NULL
for(int i = 0; i < 4; i++){
pthread_join(ctid[i], NULL);
pthread_join(dtid[i], NULL);
}
//销毁互斥量和条件变量
pthread_mutex_destroy(&mutex);
pthread_cond_destroy(&cond);
return 0;
}
生产者与消费者模型
介绍
- 概念:是一种典型的 设计模式,针对有大量数据产生及处理的场景;
生产者消费者模式就是通过一个容器来解决生产者和消费者的强耦合问题 (二者处理能力不均衡):生产者和消费者彼此之间不直接通讯,而通过阻塞队列来进行通讯,所以生产者生产完数据之后不用等待消费者处理,直接扔给阻塞队列,消费者不找生产者要数据,而是直接从阻塞队列里取,阻塞队列就相当于一个缓冲区,平衡了生产者和消费者的处理能力,这个阻塞队列就是用来给生产者和消费者解耦合的;
- 优势:支持解耦合、支持忙闲不均、支持并发;
- 原理:生产者与消费者这两种角色的线程 + 线程安全的队列(阻塞队列)
- 线程安全:
- 生产者与生产者:不能将数据放到同一队列节点中——互斥;
- 消费者与消费者:不能从同一队列节点来拿取数据——互斥;
- 生产者与消费者:
- 不能同时访问同一个节点——互斥;
- 消费者没有资源了生产者要生产,生产者生产满了消费者要使用——同步;
- 线程安全的阻塞队列的简单实现:在多线程编程中阻塞队列 (Blocking Queue) 是一种常用于实现生产者和消费者模型的数据结构。其与普通的队列区别在于,当队列为空时,从队列获取元素的操作将会被阻塞,直到队列中被放入了元素;当队列满时,往队列里存放元素的操作也会被阻塞,直到有元素被从队列中取出 (以上的操作都是基于不同的线程来说的,线程在对阻塞队列进程操作时会被阻塞)
- 线程安全:
实现
#include <iostream>
#include <queue>
#include <pthread.h>
//设计的阻塞队列的最大容量
#define MAX_QUEUE 5
//阻塞队列的实现,这其中要保证数据访问的安全性
class BlockQueue{
private:
//阻塞队列的最大容量
int _capacity;
//创建队列来存放数据
std::queue<int> _queue;
//设置一个互斥量,设置生产者和消费者的条件变量
pthread_mutex_t _mutex;
pthread_cond_t _cond_pro;
pthread_cond_t _cond_cus;
public:
//构造函数
BlockQueue(int cap = MAX_QUEUE)
:_capacity(cap)
{
//初始化各种数据
pthread_mutex_init(&_mutex, NULL);
pthread_cond_init(&_cond_pro, NULL);
pthread_cond_init(&_cond_cus, NULL);
}
//析构函数
~BlockQueue() {
//销毁各种变量
pthread_mutex_destroy(&_mutex);
pthread_cond_destroy(&_cond_pro);
pthread_cond_destroy(&_cond_cus);
}
//存入数据
bool Push(int data) {
//存放之前先加锁
pthread_mutex_lock(&_mutex);
//判断是否存储满了
while (_queue.size() == _capacity) {
//如果满了则阻塞等待消费者进行消费
pthread_cond_wait(&_cond_pro, &_mutex);
}
//如果没有满,则将数据存入
_queue.push(data);
//然后唤醒消费者
pthread_cond_signal(&_cond_cus);
//解锁
pthread_mutex_unlock(&_mutex);
return true;
}
//取出数据
bool Pop(int *data) {
//加锁
pthread_mutex_lock(&_mutex);
//判断队列是否为空
while (_queue.empty()) {
//如果为空则阻塞等待生产者产生数据
pthread_cond_wait(&_cond_cus, &_mutex);
}
//拿出数据,并进行数据出队
*data = _queue.front();
_queue.pop();
//唤醒生产者
pthread_cond_signal(&_cond_pro);
//解锁
pthread_mutex_unlock(&_mutex);
return true;
}
};
//生产者执行流程
void *productor(void *arg){
//拿到阻塞队列
BlockQueue *q = (BlockQueue*)arg;
//存放数据
int i = 0;
while(1) {
q->Push(i);
printf("%p-push data:%d\n", pthread_self(), i++);
}
return NULL;
}
//消费者执行流程
void *customer(void *arg){
//获取阻塞队列
BlockQueue *q = (BlockQueue*)arg;
//循环取出数据
while(1) {
int data;
q->Pop(&data);
printf("%p-get data:%d\n", pthread_self(), data);
}
return NULL;
}
int main (int argc, char *argv[]){
//创建阻塞队列
BlockQueue q;
//创建四个线程来执行生产者流程
int count = 4, ret;
pthread_t ptid[4], ctid[4];
for (int i = 0; i < count; i++) {
ret = pthread_create(&ptid[i],NULL,productor,&q);
if (ret != 0) {
printf("thread create error\n");
return -1;
}
}
//创建四个线程来执行消费者流程
for (int i = 0; i < count; i++) {
ret = pthread_create(&ctid[i],NULL,customer,&q);
if (ret != 0) {
printf("thread create error\n");
return -1;
}
}
//循环等待线程退出
for (int i = 0; i < count; i++) {
pthread_join(ptid[i], NULL);
pthread_join(ctid[i], NULL);
}
return 0;
}
信号量
基本概念
- 本质:计数器;
- 作用:实现进程或线程间的同步与互斥;
- 操作:
- P 操作:计数 -1,计数小于 0,则阻塞执行流;
- V 操作:计数 +1,唤醒一个阻塞的执行流;
- 同步实现:通过计数器对资源进行计数
- 在获取资源之前,进行 P 操作,产生资源之后,进行 V 操作;
- 互斥的实现:初始化初值为 1,表示资源只有一个;
- 在访问之前进行 P 操作,访问资源完毕之后进行 V 操作;
接口认识
- 定义信号量
sem_t sem;
- 初始化信号量
int sem_init(sem_t* sem, int pshared, unsigned int value);
:根据初始化的值来充当不同的角色;sem
:信号量;pshared
:若果是 0,则代表了线程间信号量,如果是非 0,则代表了进程间信号量;value
:要设置的初值,互斥量——1,条件变量——资源数;
- P 操作:将信号值减一,有以下三种办法:
int sem_wait(sem_t* sem);
:阻塞等待,如果不能进行 P 操作,则一直等待直到可以进行;int sem_trywait(sem_t* sem);
:非阻塞等待,如果不能进行 P 操作,则报错返回;int sem_timedwait(sem_t* sem, struct timespec* timeout);
:阻塞一定时间,再该时间内没有进行操作,则时间一过报错返回;
- V 操作:将信号值加一
int sem_post(sem_t* sem);
- 销毁信号量
int sem_destory(sem_t* sem);
举例
- 使用信号量来创建一个消费者与生产者模型,此处我们使用循环队列来建立该模型的阻塞队列:
#include <iostream>
#include <vector>
#include <pthread.h>
#include <semaphore.h>
//设计的阻塞队列的最大容量
#define MAX_QUEUE 5
//创建阻塞队列,并且要保证在访问数据时的安全性
class RingQueue {
private:
//阻塞队列的最大容量
int _capacity;
//循环队列的读下标
int _step_read;
//循环队列的写下标
int _step_write;
//我们创建一个顺序表来表示循环队列
std::vector<int> _arry;
//创建一个互斥信号量
sem_t _sem_lock;
//创建两个代表资源数的信号量
//这个代表了空闲节点的信号量
sem_t _sem_idle;
//这个代表了数据节点的信号量
sem_t _sem_data;
public:
//构造函数,初始化容量,读写下标,顺序表结点个数
RingQueue(int cap = MAX_QUEUE)
:_capacity(cap)
,_step_read(0)
, _step_write(0)
, _arry(cap)
{
//初始化三个信号量:sem_init(信号量,共享标志, 初值)
sem_init(&_sem_lock, 0, 1);//是互斥量,所以初值为 1
sem_init(&_sem_idle, 0, cap);//是空闲结点个数,所以为容量
sem_init(&_sem_data, 0, 0); //是数据结点个数,所以为 0
}
//析构函数,释放三个信号量
~RingQueue(){
sem_destroy(&_sem_lock);
sem_destroy(&_sem_idle);
sem_destroy(&_sem_data);
}
//向阻塞队列中写入数据
bool Push(int data) {
sem_wait(&_sem_idle);//P操作:空闲空间数量减一,此时空闲个数小于0,则阻塞
sem_wait(&_sem_lock);//加锁
_arry[_step_write] = data;//写入数据
_step_write = (_step_write+1) % _capacity;//下标进行循环增长
sem_post(&_sem_lock);//解锁
sem_post(&_sem_data);//V操作:数据空间数量加一,如果满了则阻塞,并唤醒消费者
return true;
}
//从阻塞队列中拿出数据
bool Pop(int* data) {
sem_wait(&_sem_data);//P操作:数据空间数量减一,此时数据个数小于0,则阻塞
sem_wait(&_sem_lock);//加锁
*data = _arry[_step_read];//拿出数据
_step_read = (_step_read + 1) % _capacity;//下标进行循环增长
sem_post(&_sem_lock);//解锁
sem_post(&_sem_idle);//V操作:空闲空间数量加一,如果满了则阻塞,并唤醒生产者
return true;
}
};
//生产者执行流程
void *productor(void *arg){
//阻塞队列是由参数传进来的,所以先进行转换
RingQueue* q = (RingQueue*)arg;
//生产者开始循环生产数据并存入阻塞队列中
int i = 0;
while(1) {
q->Push(i);
printf("%p-push data:%d\n", pthread_self(), i++);
}
return NULL;
}
void *customer(void *arg){
//阻塞队列是由参数传进来的,所以先进行转换
RingQueue *q = (RingQueue*)arg;
//消费者循环从队列中取出数据进行处理
while(1) {
int data;
q->Pop(&data);
printf("%p-get data:%d\n", pthread_self(), data);
}
return NULL;
}
int main (int argc, char *argv[]){
//创建阻塞队列
RingQueue q;
//分别创建四个生产者线程与四个消费者线程
int count = 4, ret;
pthread_t ptid[4], ctid[4];
for (int i = 0; i < count; i++) {
ret = pthread_create(&ptid[i],NULL,productor,&q);
if (ret != 0) {
printf("thread create error\n");
return -1;
}
}
for (int i = 0; i < count; i++) {
ret = pthread_create(&ctid[i],NULL,customer,&q);
if (ret != 0) {
printf("thread create error\n");
return -1;
}
}
//等待四个线程的退出
for (int i = 0; i < count; i++) {
pthread_join(ptid[i], NULL);
pthread_join(ctid[i], NULL);
}
return 0;
}
- 注意:
- 在之前学习数据结构的时候,我们学到循环队列,当时说道环形结构起始状态和结束状态都是一样的,不好判断为空或者为满,所以可以通过加计数器或者标记位来判断满或者空;另外也可以预留一个空的位置,作为满的状态;但是此处我们就可以利用信号量本身的计数功能来进行判满或判空,十分方便;
push
或者pop
操作中,一定要先进行 P / V 操作,然后再进行加锁,如果反着来的话,就会造成:在加了锁之后进行 P 操作时,如果阻塞队列满了,那么就不能再生产了,此时需要进行 V 操作之后才能进行生产,但是因为已经加锁了,所以无法进行 V 操作,所以就会造成死锁的情况;
信号量与条件变量区别
- 条件变量的资源获取判断条件需要程序员自己进行,而信号量自身含有计数操作,所以可以直接使用,不用判断;
- 条件变量需要搭配互斥锁进行线程安全的操作,而信号量不用;
一些锁的介绍
读写者模型
- 读写锁:在编写多线程的时候,有一种情况是十分常见的,那就是有些公共数据修改的机会较少,但是它们读的机会非常多,通常而言,在读的过程中,往往伴随着查找的操作,中间耗时很长,如果我们给这种代码段加锁,会极大地降低我们程序的效率。因此出现了读写锁,可以专门处理这种多读少写的情况;
- 当前锁的各种状态下,进行读写锁请求之后所产生的行为:
当前锁状态 | 读锁请求 | 写锁请求 |
---|---|---|
无锁 | 可以 | 可以 |
读锁 | 可以 | 阻塞 |
写锁 | 阻塞 | 阻塞 |
零碎的锁
- 悲观锁:在每次取数据时,总是担心数据会被其他线程修改,所以会在取数据前先加锁,此时当其他线程想要访问数据时,被阻塞挂起;
- 乐观锁:每次取数据时候,总是乐观的认为数据不会被其他线程修改,因此不上锁。但是在更新数据前,会判断其他数据在更新前有没有对数据进行修改。主要采用两种方式:版本号机制和 CAS 操作;
- CAS 操作:当需要更新数据时,判断当前内存值和之前取得的值是否相等,如果相等则用新值更新,若不等则失败,失败则重试,一般是一个自旋的过程,即不断重试;
- 自旋锁:当条件不满足时,占据 CPU 资源不释放,对访问条件是否满足进行循环判断,直到条件满足,则进行处理。适用于临界资源访问操作时间较短的操作;