Linux:死锁与生产者消费者模型解析

目录

1.死锁

2.生产者消费者模型

3.基于BlockingQueue的生产者消费者模型


1.死锁

死锁是指在一组进程中的各个进程均占有不会释放 的资源,但因互相申请 被其他进程所站用不会释放的资源而处于的一种永久等待状态。

上图就是发生了死锁,当然一个线程一把锁也可能发生死锁,例如一个线程申请同一把锁两次,第二次就会把自己阻塞等待,发生死锁。

死锁四个必要条件(都满足才会产生,可参考上图)

  • 互斥条件:一个资源每次只能被一个执行流使用
  • 请求与保持条件:一个执行流因请求资源而阻塞时,对已获得的资源保持不放
  • 不剥夺条件:一个执行流已获得的资源,在末使用完之前,不能强行剥夺,即其他前程不能直接调用 pthread_mutex_unlock() 剥夺,然后给自己加锁。
  • 循环等待条件:若干执行流之间形成一种头尾相接的循环等待资源的关系

避免死锁

  • 破坏死锁的四个必要条件。破坏互斥条件即不用锁,就不会有死锁。破环请求与保持就是申请第二把锁失败后,把第一把也释放掉,回退到没申请锁之前。循环等待就是一直想要对方的锁资源,所以建议按照同样的次序一次申请完锁,即第二四点。
  • 加锁顺序一致
  • 避免锁未释放的场景
  • 资源一次性分配(对一个临界资源不要重复加锁,解锁,一次访问完)

避免死锁算法

死锁检测算法(了解)

银行家算法(了解)

2.生产者消费者模型

互斥能保证资源的安全性,但如果当前线程竞争资源的能力较强,不断地加锁解锁,可能导致其他线程永远访问不到资源,所以需要同步来较为高效地使用资源。

仓库相当于特定空间的数据结构或者容器。1个交易场所:内存空间;2种角色:生产者与消费者;这个模型中还存在3种关系:

  1. 生产者与生产者 -> 互斥
  2. 消费者与消费者 -> 互斥
  3. 生产者与消费者 -> 互斥与同步

生产者消费者模型的本质是进行线程之间的数据传输 的。可以让生产数据和处理数据解耦 ,仓库相当于一段缓存,生产者生产好放进去,可以继续执行其他操作。消费者也可以执行自己的操作,需要数据时再读,支持并发。生产者与消费者处理速度不同也没事,支持忙闲不均 ,提高处理数据的效率,可以看后面代码。

条件变量:

为什么需要条件变量,比如我们上面学习到的抢票系统和生产者与消费者模型,当消费者不断申请锁访问临界资源票时,可能导致生产者一直无法访问临界资源提供票,这种问题就需要线程同步,让他们有一定的顺序,条件变量可以解决这一问题。

条件变量是 pthread 库提供的一种一个线程向另一个线程进行事件通知的方式,条件变量的类型是 pthread_cond_t

条件变量初始化:这里也分全局条件变量初始化和局部条件变量初始化,全局条件变量程序结束会自动释放,局部条件变量需要执行后面的销毁函数。

cpp 复制代码
全局定义
pthread_cond_t cond = PTHREAD_COND_INITIALIZER;

局部定义
int pthread_cond_init(pthread_cond_t *restrict cond,
                      const pthread_condattr_t *restrict attr);
参数:
cond:要初始化的条件变量
attr:NULL

销毁

cpp 复制代码
int pthread_cond_destroy(pthread_cond_t *cond)

等待条件满足,调用该函数的进程在该条件变量下等待

cpp 复制代码
int pthread_cond_wait(pthread_cond_t *restrict cond,
                    pthread_mutex_t *restrict mutex);
参数:
cond:要在这个条件变量上等待
mutex:互斥量,后面详细解释
pthread 的接口成功的时候返回0,失败的时候返回真

唤醒等待

cpp 复制代码
broadcast(广播) 是条件变量成立,唤醒所有线程
int pthread_cond_broadcast(pthread_cond_t *cond);

signal 使指定的条件变量成立,并唤醒一个线程
int pthread_cond_signal(pthread_cond_t *cond);

不引入临界区,单纯使用条件变量,让多个线程按照顺序执行。pthread_cond_signal 从等待条件变量队列中唤醒一个线程。当然我们控制不了具体执行顺序 ,因为无法判断第一轮的线程谁先申请到锁,但后续他们执行顺序都是一样的。显示器打印东西出现混乱,本质是显示器也是文件资源,而我们对资源没有进行保护,显示器也是临界资源。pthread_cond_broadcast 也就是之前学习到的线程一起争抢锁。对于为什么等待条件变量需要传入锁,我们后面讲。

cpp 复制代码
pthread_cond_t cond = PTHREAD_COND_INITIALIZER;
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;

void* threadRoution(void* argv)
{
    std::string name = static_cast<const char*>(argv);
    while(true)
    {   
        sleep(1);
        pthread_mutex_lock(&mutex);//可以保证显示器打印不出现混乱,显示器

        //等待主线程使条件变量成立
        pthread_cond_wait(&cond,&mutex);//?????   
        std::cout << " I am a new thread : " << name << std::endl;
        pthread_mutex_unlock(&mutex);
    }
}

int main()
{
    pthread_t t1,t2,t3;
    pthread_create(&t1,nullptr,threadRoution,(void*)"thread-1");
    pthread_create(&t2,nullptr,threadRoution,(void*)"thread-2");
    pthread_create(&t3,nullptr,threadRoution,(void*)"thread-3");

    sleep(5);//5s之后开始让条件变量成立
    while (true)
    {
        pthread_cond_signal(&cond);//唤醒一个线程
        //pthread_cond_broadcast(&cond);//全部唤醒之后, 没有顺序
        sleep(1);
    }
    pthread_join(t1,nullptr);
    pthread_join(t2,nullptr);
    pthread_join(t3,nullptr);
    return 0;
}

线程访问临界资源,首先需要锁,再判断有没有资源。如何保证顺序,在加锁和解锁中等待条件变量成立,不成立会在一个等待条件变量队列中等待,释放锁 ,其他线程因此会得到锁,按顺序进来再排队阻塞,直到条件变量成立依次唤醒,醒来时依旧需要申请锁

cpp 复制代码
pthread_cond_t cond = PTHREAD_COND_INITIALIZER;
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;

int tickets = 10000;

void* threadRoution(void* argv)
{
    std::string name = static_cast<const char*>(argv);
    while(true)
    {   
        pthread_mutex_lock(&mutex);//临界资源:显示器和tickets
        if(tickets > 0)
        {
            std::cout << name << " I am a new thread : " 
                    << tickets << std::endl;
            tickets--;
            usleep(1000);
        }
        else
        {
            std::cout << "没有票了" << name << std::endl;
            pthread_cond_wait(&cond,&mutex);//?????  
        }
        pthread_mutex_unlock(&mutex); 
        就是每一个线程在大量地申请锁释放锁,浪费锁资源
    }
}

每一个线程在大量地申请锁释放锁访问临界资源,浪费锁资源,就有可能导致生产者得到锁,无法提供资源,所以我们应该让这些消费者线程去等条件变量,不是锁 ,等有票了再去访问临界资源,就会阻塞这些线程。pthread_cond_wait(&cond,&mutex);

我们在主线程提供加票,之后加的1000张票每次被一个线程6秒内抢完,但总体他们有顺序:

cpp 复制代码
int main()
{
    pthread_t t1,t2,t3;
    pthread_create(&t1,nullptr,threadRoution,(void*)"thread-1");
    pthread_create(&t2,nullptr,threadRoution,(void*)"thread-2");
    pthread_create(&t3,nullptr,threadRoution,(void*)"thread-3");

    sleep(5);//开始执行唤醒
    while (true)
    {
        sleep(6);

        pthread_mutex_lock(&mutex);
        tickets +=  1000;//
        pthread_mutex_unlock(&mutex);

        这里会一个全部抢完,因为6秒内,其他线程在队列中还没有被唤醒
        pthread_cond_signal(&cond); 
        //pthread_cond_broadcast(&cond);//全部唤醒之后,  没有顺序
    }
    pthread_join(t1,nullptr);
    pthread_join(t2,nullptr);
    pthread_join(t3,nullptr);
    return 0;
}

所以单纯的互斥,能保证数据安全,不一定合理或者高效,也需要同步!

pthread_cond_wait

  1. 可以让线程在等待的时候,会自动释放锁,其他线程可以申请锁,所以需要传入一把锁。
  2. 线程被唤醒的时候,是在临界区被唤醒的,当线程被唤醒,在 pthread_cond_wait 返回的时候,要重新申请并持有锁
  3. 当线程被唤醒的时候,重新申请并持有锁本质也是要重新参与锁的竞争,所以 pthread_cond_broadcast 唤醒后是乱序的。

3.基于BlockingQueue的生产者消费者模型

在多线程编程中阻塞队列(Blocking Queue) 是一种常用于实现生产者和消费者模型的数据结构。其与普通的队列区别在于,当队列为空时,消费线程不能再消费;当队列满时,生产线程不能再生成。

伪唤醒问题:对应条件不足,但是线程却被唤醒了。

例如我们采用 pthread_cond_broadcast 唤醒策略,当有一个线程消费完,唤醒其他消费者,释放锁,其他消费者都可能会逐渐按照 pthread_mutex_lock 获得锁,没有条件变量唤醒时调用 pthreadread_cond_wait 的方式释放锁,都在 pthread_cond_wait 处阻塞,当只有最后一个资源被消费后,其他线程就不会在 pthread_cond_wait 处继续阻塞了,但此时已经没有资源了,所以会发生问题,**需要将 if 改成 while,因为while本身也具有条件判断的能力。**生产者也是同样的道理,我们可以看下面代码。

自己实现的BlockingQueue.hpp:

cpp 复制代码
#pragma once 

#include <iostream>
#include <queue>
#include <pthread.h>

const int defaultcap = 5;

template<class T>
struct BlockQueue
{
public:
    BlockQueue(int cap = defaultcap):_capacity(cap)
    {
        pthread_mutex_init(&_mutex,nullptr);
        pthread_cond_init(&_p_cond,nullptr);
        pthread_cond_init(&_c_cond,nullptr);
    }

    bool Push(const T&in)//生产者
    {
        pthread_mutex_lock(&_mutex);
        while(IsFull())
        {
            //阻塞等待
            pthread_cond_wait(&_p_cond,&_mutex);
        }
        //不加else也行
            _q.push(in);
            //if(_q.size() > _productor_water_line)
            pthread_cond_signal(&_c_cond);//有产品唤醒对方消费
            //当对方已经醒着的时候会忽略这个信号
            //在锁内外唤醒都行,因为消费者消费完等待时会释放锁。
        pthread_mutex_unlock(&_mutex);

        return true;
    }

    bool Pop(T* out)//消费者
    {
        pthread_mutex_lock(&_mutex);
        while(IsEmpty())
        {
            //阻塞等待
            pthread_cond_wait(&_c_cond,&_mutex);
        }
        //不加else也行
            *out = _q.front();
            _q.pop();
            pthread_cond_signal(&_p_cond);//消费产品了唤醒对方生产
        pthread_mutex_unlock(&_mutex);
        return true;
    }

    bool IsFull()
    {
        return _q.size() == _capacity;
    }
    bool IsEmpty()
    {
        return _q.size() == 0;
    }
    ~BlockQueue()
    {
        pthread_mutex_destroy(&_mutex);
        pthread_cond_destroy(&_p_cond);
        pthread_cond_destroy(&_c_cond);
    }

private:
    std::queue<T> _q;//要有容量上限,STL
    int _capacity;//_q.size() == _capacity 满了 
    pthread_mutex_t _mutex; //多线程访问需要对queue保护
    
    //防止一方线程不断地申请访问锁,另一方申请不到,例如满了之后消费者无法消费
    pthread_cond_t _p_cond;//没了之后 让生产者等待   消费者给生产者
    pthread_cond_t _c_cond;//满了之后 让消费者等待   生产者给消费者
    //两个线程互相唤醒
    也可以定制唤醒策略比如达到一定程度再唤醒
    //int _productor_water_line = _capacity / 3;
};

重点是对临界区加锁,然后同步通过双方互相 pthread_cond_signal 发送条件变量唤醒。对已经唤醒的线程发送唤醒信号,是会被忽略的,不影响。LockGuard 也可以在这里使用不用调用解锁的函数了,及时 pthread_cond_signal 中解锁了,唤醒时还是会上锁的,不用担心。

测试代码,Main.cc,一个生产者一个消费者。

cpp 复制代码
#include "BlockQueue.hpp"
#include <iostream>
#include <unistd.h>
#include <pthread.h>
#include <time.h>
#include <cstdint>

void* consumer(void* argv)
{
    //生产者与消费者同时看到了一份公共资源
    BlockQueue<int>* bq = static_cast<BlockQueue<int>*>(argv);

    while(true)
    {
        int data = 0;
        bq->Pop(&data);
        std::cout << "consumer data: "<< data << std::endl;
        //消费者没有sleep
        sleep(1);
    }
}

void* productor(void* argv)
{
    BlockQueue<int>* bq = static_cast<BlockQueue<int>*>(argv);

    while(true)
    {
        int data = rand() % 10 + 1; //[1,10] 

        bq->Push(data);
        std::cout << "prodector data: "<< data << std::endl;
        //sleep(1);
    }
}

int main()
{
    srand((uint16_t)time(nullptr)^getpid()^pthread_self());//更随机
    BlockQueue<int> *bq = new BlockQueue<int>();
    pthread_t c, q;
    pthread_create(&c,nullptr,consumer,bq);
    pthread_create(&q,nullptr,productor,bq);

    pthread_join(c,nullptr);
    pthread_join(q,nullptr);

    return 0;
}

因为阻塞队列是队列,所以这里也是 FIFO。

生产者与消费者模型是如何提高效率的:一定不要忽略生产前和消费后还需要处理其他事情也要时间,比如后面要学习网络,我们从网络中取数据也需要时间,这个时间消费者就可以同时处理之前从网络获取的数据,并发就体现出来了。

实现多生产者多消费者:

我们写的 BlockQueue 已经可以实现多生产者多消费者,只需要多创建几个生产者线程和消费者线程。我们也可以通过传入结构体的方式传入更多信息,多传入一个线程名字Main.cc

cpp 复制代码
#include "BlockQueue.hpp"
#include <iostream>
#include <unistd.h>
#include <pthread.h>
#include <time.h>
#include <cstdint>
#include "Task.hpp"

class ThreadData
{
public:
    BlockQueue<Task>* bq;
    std::string name;
};

void* consumer(void* argv)
{
    //生产者与消费者同时看到了一份公共资源
    ThreadData* ctd = static_cast<ThreadData*>(argv);
    BlockQueue<Task>* bq = ctd->bq;//阻塞队列
    std::string name = ctd->name;

    while(true)
    {
        //sleep(1);
        Task t;
        bq->Pop(&t);
        //t.Run();
        t();//仿函数 
        std::cout << " consumer data: " << t.PrintResult() << name << std::endl;
    }
}

void* productor(void* argv)
{
    ThreadData* ptd = static_cast<ThreadData*>(argv);
    BlockQueue<Task>* bq = ptd->bq;
    std::string name = ptd->name;

    while(true)
    {
        int data1 = rand() % 10; //[1,10] 
        usleep(rand()%123);//让随机数变化一下
        int data2 = rand() % 10;
        usleep(rand()%123);
        char oper = rand() % opers.size();
        Task t(data1,data2,opers[oper]);
        //for debug
        std::cout << "prodector task: " << t.PrintTask() << name << std::endl;
        bq->Push(t);
        sleep(1);

    }
}

int main()
{
    srand((uint16_t)time(nullptr)^getpid()^pthread_self());//更随机
    BlockQueue<Task> *bq = new BlockQueue<Task>();//不能直接传入可以扩容的queue,因为不是线程安全的
    pthread_t c[3], p[2];

    //3个消费者
    ThreadData* ctd = new ThreadData;
    ctd->bq = bq;
    ctd->name = "c thread - 1 ";
    pthread_create(&c[0],nullptr,consumer,ctd);
    ThreadData* ctd1 = new ThreadData;
    ctd1->bq = bq;
    ctd1->name = "c thread - 2 ";
    pthread_create(&c[1],nullptr,consumer,ctd1);
    ThreadData* ctd2 = new ThreadData;
    ctd2->bq = bq;
    ctd2->name = "c thread - 3 ";
    pthread_create(&c[2],nullptr,consumer,ctd2);

    ThreadData* ptd1 = new ThreadData;
    ptd1->bq = bq;
    ptd1->name = "p thread - 1 ";
    pthread_create(&p[0],nullptr,productor,ptd1);
    ThreadData* ptd2 = new ThreadData;
    ptd2->bq = bq;
    ptd2->name = "p thread - 2 ";
    pthread_create(&p[0],nullptr,productor,ptd1);
    
    pthread_join(c[0],nullptr);
    pthread_join(p[0],nullptr);
    pthread_join(c[1],nullptr);
    pthread_join(p[1],nullptr);
    pthread_join(c[2],nullptr);

    return 0;
}

Task.cc:

cpp 复制代码
#pragma once
#include <string>

const int defaultvalue = 0;
enum
{
    ok = 0,
    div_zero,
    mod_zero,
    unknow
};

const std::string opers = "+-*/%()&";

class Task
{
public:
    Task(){};
    Task(int x,int y,char op)
        :data_x(x),data_y(y),oper(op),result(defaultvalue),code(ok)
    {}

    void Run()
    {
        switch (oper)
        {
        case '+':
            result = data_x + data_y;
            break;
        case '-':
            result = data_x - data_y;
            break;
        case '*':
            result = data_x * data_y;
            break;
        case '/':
        {
            if (data_y == 0)
                code = div_zero;
            else
                result = data_x / data_y;
        }
        break;
        case '%':
        {
            if (data_y == 0)
                code = mod_zero;
            else
                result = data_x / data_y;
        }
        break;
        default:
            code = unknow;
            break;
        }
    }

    void operator()()
    {
        Run();
    }

    std::string PrintTask()
    {
        std::string s;
        s = std::to_string(data_x);
        s+= oper;
        s+= std::to_string(data_y);
        s+=" = ?";
        
        return s;
    }

    std::string PrintResult()
    {
        std::string s;
        s = std::to_string(data_x);
        s+= oper;
        s+= std::to_string(data_y);
        s+=" = ";
        s = std::to_string(result);
        s+= " [";
        s+= std::to_string(code);
        s+=']';
        return s;
    }

    int data_x;
    int data_y;
    char oper;
    int result;
    int code;
};

输出结果:

为什么这里消费者线程调度会有一定的顺序,生产者线程调度没有呢,因为最开始我们的 BlockQueue是空的,我们无论哪个消费者进入都会在 pthread_cond_wait 出排队等待,释放锁,所以当有资源是会 FIFO 调度,而生产就没有这个排队限制,会竞争锁。如果 BlockQueue 不为空,没有条件变量的限制,消费者能直接执行完释放锁,也是无序的。

本篇结束!

相关推荐
RisunJan1 小时前
Linux命令-rsync(远程/本地文件同步 —— 增量传输的备份与镜像神器)
linux·运维·服务器
爆浇牛肉面2 小时前
Linux进阶命令:测开必学的curl和jq
linux
Sinclair2 小时前
安企CMS的安装-常见安装问题排查
运维·后端
Sinclair2 小时前
安企CMS的安装-安装引导与初始化
运维·后端
Sinclair2 小时前
安企CMS的安装-源码编译安装
运维·后端·go
exm-zem2 小时前
从C语言到C++:C++入门1
c语言·c++
Sinclair2 小时前
安企CMS的安装-同一服务器多站点部署
运维·后端
Sinclair2 小时前
安企CMS的安装-Docker 部署
运维·docker·容器
Sinclair2 小时前
安企CMS的安装-服务器手动部署
运维·后端