【Linux】线程同步和死锁

目录

死锁

什么是死锁

构成死锁的四个必要条件

如何避免死锁

线程同步

同步的引入

同步的方式

条件变量

条件变量的使用

整体代码


死锁

什么是死锁

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

        例如进程两个锁mtx1和mtx2,进程A执行某一段代码需要先申请mtx1,再申请mtx2;而进程B执行对应的代码需要现申请mtx2,再申请mtx1.

        某个时刻,进程A和B同时运行,进程A拿到了mtx1,进程B拿到了mtx2,但紧接着,进程A需要mtx2,但此时这把锁被进程B所占用,无法申请,便阻塞等待进程B的完成。而进程B需要mtx1,但是此时被A占用,需要等待进程A的完成,也阻塞在了这里。于是便造成了死锁。

        

构成死锁的四个必要条件

  • 互斥条件: 一个资源每次只能被一个执行流使用,其他进程无法同时访问该资源。
  • 请求与保持条件:即进程在请求资源时可以保持已占有的资源,即不释放自己原本的资源
  • 不剥夺条件:已经获得资源的进程不能被强行剥夺其他进程所拥有的资源,只有自愿释放
  • 循环等待条件:若干进程之间形成一种头尾相连的循环等待资源关系。如A->B,B->A.

如何避免死锁

     我们知道了构成死锁的那个四个必要条件,只要破坏其中任意一个 条件即可:

  • 破坏互斥条件:尽量避免使用互斥资源,或者采用不同的资源访问方式,如读写锁,允许多个进程或线程同时访问某些资源。
  • 破坏请求与保持条件:如果申请多个锁失败,则释放自己已经拥有的资源
  • 破坏不剥夺条件:引入资源抢占机制,即允许操作系统对进程已获得的资源进行抢占。当其他进程紧迫需要某个资源时,系统可以终止或暂停某个进程,将其持有的资源释放分配给需要的进程。
  • 打破环路等待条件:采用全局资源排序策略,为每个资源指定一个唯一的编号,然后要求进程只能按照编号递增的顺序请求资源,这样可以避免环路等待的发生。

   以上所说的"资源"都也可以理解为锁。

线程同步

同步的引入

        上一章我们说的        

        1.多线程然后抢票的例子,我们发现虽然有多个线程,但是每一次基本上都是那一个线程在抢(比如优先级可能更高),其它线程抢不到,这就是一个线程频繁地申请到资源,造成别的线程饥饿问题。

        2.假设一个资源暂时没有了,而线程依旧在竞争锁,然后访问资源,访问不到然后释放锁没就这样一直进行,但此时也没有资源可用。这样就太过于浪费了。

        以上这些操作都是正确的,但是是不合理的!

        所以为了解决上面这系列问题,便引入了同步:主要是为了解决 访问临界资源合理性问题的.

按照一定的顺序,进行临界资源的访问

        1.对于问题一我们可以这样:当一个线程申请到资源后,使用完之后,排到其它线程后面,让其他线程先访问,如此进行下去。

        2.对于问题二,我们可以暂时每个线程发个号,当有资源时,再按照号的顺序来访问资源,而不是互相不正当竞争这份资源

所以线程同步的是:线程同步是指在并发编程中,通过协调多个线程的执行顺序以及对共享资源的访问来保证线程之间的正确交互

同步的方式

条件变量

        当我们申请临界资源时 ---> 要对临界资源是否存在做检测 ---> 检测的本质:也是访问临界资源 --->结论:对临界资源的检测,也一定是在加锁和解锁之间的。

        既然这样,那检测依然需要频繁地申请和释放锁,那么有没有办法让线程检测到资源不就绪的时候:

        a.不要让线程自己再频繁检测了,而是等待

        b.当条件就绪的时候,通知对应的线程,让它来进行资源的申请与访问。

为了满足上面的说话,这里就有引入条件变量。

        条件变量(Condition Variable)是一种同步原语,常用于多线程编程中进行线程间的等待和通知。它用于实现线程之间的同步和协作,使得一个线程可以等待某个条件的满足并被其他线程通知唤醒。

条件变量的使用

        使用条件变量需要配合互斥锁(pthread_mutex_t)来保证线程的安全操作。一般的使用步骤如下:

        首先我们要先创建一个条件变量,数据类型为 pthread_cond_t,同时也要创建互斥锁。

        

    pthread_mutex_t mtx;
    pthread_cond_t cond;

       初始化条件变量和互斥锁:互斥锁我们说了初始化方式了,这里说初始化条件变量的函数:

 pthread_cond_init,该函数原型如下:

       int pthread_cond_init(pthread_cond_t *restrict cond,
              const pthread_condattr_t *restrict attr);

        第一个参数为条件变量,第二个参数为条件变量属性,一般设为NULL。

        如果创建的条件变量是全局的,那么可以用下面的方法进行初始化:

       pthread_cond_t cond = PTHREAD_COND_INITIALIZER;

所以初始化代码如下: 

    pthread_cond_init(&cond,nullptr);//初始化条件变量
    pthread_mutex_init(&mtx,nullptr);//初始化锁

紧接着,我们创建4个线程,然后再创建一个结构体,里面包含了线程名,该线程调用的方法,条件变量和互斥锁,然后编写一个构造函数来初始化这些:

typedef void(*func_t)(const string& name,pthread_mutex_t* pmtx, pthread_cond_t* pcond);
class ThreadData
{
public:
    ThreadData(const string& name,func_t func,pthread_mutex_t* pmtx, pthread_cond_t* pcond)
    :_name(name),_func(func),_pmtx(pmtx),_pcond(pcond)
    {}
public:
    string _name;//线程名
    func_t _func;//该线程对应的回调方法
    pthread_mutex_t* _pmtx;//互斥锁
    pthread_cond_t* _pcond;//条件变量
};

        然后开始编写每个线程的回调方法,这里其实不能很好地展示条件变量中锁的作用,我们需要在下一章生产者与消费者模型时,才能好好看出作用.

        这里每个线程方法,我们先利用pthread_cond_wait进行阻塞等待,当资源准备就绪的时候,才会继续向后执行。然后后面输出一条语句,

        该函数原型如下:

int pthread_cond_wait(pthread_cond_t *cond, pthread_mutex_t *mutex);

        函数pthread_cond_wait用于使线程进入等待状态,并且会原子性地释放由mutex指定的互斥锁,进入等待状态后会阻塞等待,直到其他线程使用相同的条件变量调用pthread_cond_signal或pthread_cond_broadcast时,被唤醒并重新获得互斥锁。调用前需要确保已经加锁。返回时会重新获得互斥锁。

        其中第一个参数为条件变量,第二个参数为互斥锁。具体什么作用,下节课会讲。

        这是线程1的回调方法,线程2,3,4都是同样地,只不过名字不同。

void func1(const string& name,pthread_mutex_t* pmtx, pthread_cond_t* pcond)
{
    while(true)
    {
        pthread_mutex_lock(pmtx);
        //if(临界资源不就绪) wait之前一般会进行检测,这里由于无法很好的模拟场景,就暂时不加if
        pthread_cond_wait(pcond,pmtx);//默认该线程在执行的时候,wait代码被执行,当前线程会立即被阻塞
        cout << name << "running ..." << endl;
        pthread_mutex_unlock(pmtx);
    }
}

        现在创建好线程后,每个线程都被阻塞在了pthread_mutex_wait接口这里,所以我们需要再主函数中唤醒这些线程,共有两种方式:

pthread_cond_signal

int pthread_cond_signal(pthread_cond_t *cond);

参数为条件变量,至于唤醒哪一个线程,这是由调度器决定的,但顺序一定是固定的,当我们运行起来程序后:

        这样便保证了线程同步。 

这是一个一个线程的唤醒,如果我们想唤醒所有线程,这就需要用到pthread_cond_broadcast,

该函数原型如下:

int pthread_cond_broadcast(pthread_cond_t *cond);

参数同样也为条件变量,函数pthread_cond_broadcast用于广播条件变量的信号,即唤醒所有等待此条件变量的线程。

 这样便一次能唤醒所有线程继续执行了。


一切完成之后,我们需要在最后销毁释放条件变量和互斥锁,函数为pthread_cond_destroy,

函数原型为:

       int pthread_cond_destroy(pthread_cond_t *cond);

参数同样为定义的条件变量,传进去之后,即可释放条件变量。


整体代码

        以上便是条件变量的一个大致使用流程,具体的理解下一章生产者消费者模型会讲解,这列理解了条件变量的用法即可。

        可以拷贝到自己平台下运行,编译时记得加上-lpthread,如下:

g++ -o mythread mythread.cc -lpthread

代码:

#include<iostream>
#include<string>
#include<stdlib.h>
#include<unistd.h>
#include<pthread.h>

using namespace std;

#define PTHREAD_NUM 4

typedef void(*func_t)(const string& name,pthread_mutex_t* pmtx, pthread_cond_t* pcond);
class ThreadData
{
public:
    ThreadData(const string& name,func_t func,pthread_mutex_t* pmtx, pthread_cond_t* pcond)
    :_name(name),_func(func),_pmtx(pmtx),_pcond(pcond)
    {}
public:
    string _name;
    func_t _func;
    pthread_mutex_t* _pmtx;
    pthread_cond_t* _pcond;
};

void func1(const string& name,pthread_mutex_t* pmtx, pthread_cond_t* pcond)
{
    while(true)
    {
        pthread_mutex_lock(pmtx);
        //if(临界资源不就绪) wait之前一般会进行检测,这里由于无法很好的模拟场景,就暂时不加if
        pthread_cond_wait(pcond,pmtx);//默认该线程在执行的时候,wait代码被执行,当前线程会立即被阻塞
        cout << name << "running ..." << endl;
        pthread_mutex_unlock(pmtx);
    }
}
void func2(const string& name,pthread_mutex_t* pmtx, pthread_cond_t* pcond)
{
    while(true)
    {
        pthread_mutex_lock(pmtx);
        pthread_cond_wait(pcond,pmtx);//默认该线程在执行的时候,wait代码被执行,当前线程会立即被阻塞
        cout << name << "running ..." << endl;
        pthread_mutex_unlock(pmtx);
    }
}
void func3(const string& name,pthread_mutex_t* pmtx, pthread_cond_t* pcond)
{
    while(true)
    {
        pthread_mutex_lock(pmtx);
        pthread_cond_wait(pcond,pmtx);//默认该线程在执行的时候,wait代码被执行,当前线程会立即被阻塞
        cout << name << "running ..." << endl;
        pthread_mutex_unlock(pmtx);
    }
}
void func4(const string& name,pthread_mutex_t* pmtx, pthread_cond_t* pcond)
{
    while(true)
    {
        pthread_mutex_lock(pmtx);
        pthread_cond_wait(pcond,pmtx);//默认该线程在执行的时候,wait代码被执行,当前线程会立即被阻塞
        cout << name << "running ..." << endl;
        pthread_mutex_unlock(pmtx);
    }
}


void* Entry(void* args)
{
    ThreadData* td = (ThreadData*)args;//td在每一个线程自己私有的栈空间保存
    td->_func(td->_name,td->_pmtx,td->_pcond);
    delete td;

    return nullptr;
}
int main()
{
    pthread_mutex_t mtx;
    pthread_cond_t cond;

    pthread_mutex_init(&mtx,nullptr);
    pthread_cond_init(&cond,nullptr);


    pthread_t tids[PTHREAD_NUM];
    //定义四个线程的回调方法
    func_t funcs[PTHREAD_NUM] = {func1,func2,func3,func4};
    for(int i = 0; i < PTHREAD_NUM; i++)
    {
        string name = "thread ";
        name += to_string(i+1);
        ThreadData* td = new ThreadData(name,funcs[i],&mtx,&cond);
        pthread_create(tids+i,nullptr,Entry,(void*)td);
    }

    //这里为了方便演示pthread_cond_wait,在没有用signal或broadcast唤醒前,一直处于阻塞状态
    sleep(5);
    //控制线程
    while(true)
    {
        cout << "wake up thread run code ..." << endl;
        //pthread_cond_signal(&cond);//唤醒一个线程
        pthread_cond_broadcast(&cond);//唤醒全部线程
        sleep(1);

    }

    for(int i = 0; i < PTHREAD_NUM; i++)
    {
        pthread_join(tids[i],nullptr);
    }

    pthread_mutex_destroy(&mtx);
    pthread_cond_destroy(&cond);
    return 0;
}

猜你喜欢

转载自blog.csdn.net/weixin_47257473/article/details/132257979
今日推荐