饥饿问题,线程同步/竞态条件概念,条件变量的引入,概念,接口(初始化,销毁,等待,唤醒),示例代码(如何封装入口函数,唤醒的2种方式,访问临界资源(为什么需要锁))

目录

引入

饥饿问题 -- 线程同步

介绍

解决 

等待资源就绪 -- 条件变量

介绍 

解决 

概念

条件变量 

线程同步

竞态条件

条件变量接口

返回值

初始化

pthread_cond_init()

函数原型

cond

attr

pthread_cond_destroy()

PTHREAD_COND_INITIALIZER

等待条件满足

pthread_cond_wait()

函数原型

cond

mutex 

唤醒线程

pthread_cond_broadcast

函数原型

pthread_cond_signal

函数原型

示例代码

让多个线程执行不同任务

代码

介绍

封装统一入口函数

代码

介绍

唤醒等待同一条件变量的线程(两种方式)

引入

代码 

唤醒全部线程

唤醒单个线程 

介绍

临界资源达成某种条件,退出线程(卡住,无法退出)

代码

介绍

加入锁

代码

介绍


引入

饥饿问题 -- 线程同步

介绍

比如说在之前的代码中,可能会出现某个线程可以频繁的抢占到锁资源:

饥饿问题,线程同步/竞态条件概念,条件变量的引入,概念,接口(初始化,销毁,等待,唤醒),示例代码(如何封装入口函数,唤醒的2种方式,访问临界资源(为什么需要锁))_第1张图片

  • 因为锁资源是它释放的,如果他不用执行其他代码,自然也就比别人更快地重新循环,继续申请到锁
  • 但是这并不合理,因为还有其他线程在等待着锁
  • 如果它一个线程就完成了所有任务,那其他线程存在的意义就没有了,这就导致其他线程出现饥饿问题

解决 

  • 所以,我们需要对等待资源的线程进行管理,比如,,利用排队的原理
  • 让上一个抢到资源的线程排到队尾,这样就可以避免同一个线程频繁拿到资源的问题
  • 并且,谁先来等待这个资源,谁就排前面,而不是让线程之间自行竞争
  • 这样,访问资源的线程就具有了顺序性
  • 按照特定顺序访问临界资源的过程,就叫做线程同步

等待资源就绪 -- 条件变量

介绍 

在访问临界资源前,我们需要等待临界资源就绪,才会进行处理:

 while (true)
    {
        int n = pthread_mutex_lock(info->pmux);
        assert(n == 0);
        if (count > 0) //判断成功才会执行任务
        {
            usleep(1000); // 模拟可能花费的时间
 
            cout << info->name << " : " << count << endl;
            --count;
            n = pthread_mutex_unlock(info->pmux);
            assert(n == 0);
        }
        else
        {
            n = pthread_mutex_unlock(info->pmux);
            assert(n == 0);
            break;
        }
        usleep(1000); // 模拟抢票成功后的花费时间(为了尽量让不同线程去抢票)
    }
  • 这里我们就需要判断count,只有>0时,才会进行抢票
  • 判断也是一种访问,而访问临界资源需要在锁范围内,所以每次判断都需要申请锁

  • 如果count>0还好,申请锁后确实对临界资源进行了操作
  • 但count为0,线程还要申请锁,释放锁,就很没必要
  • 并且我们并不确定此时count究竟是多少,有没有>0,线程就需要一直进行申请锁,判断,释放锁的过程,非常浪费资源和时间

解决 

  • 我们可以参考现实中的处理方式,比如:
  • 当你需要某个东西,但它此时并没有,而且不确定什么时候会有
  • 难道你会每天都去店里问吗?不会的,我们一般都是留个电话,让工作人员在有的时候通知我们就行

  • 所以,类比到这里的情况,线程不需要陷入循环,来不断申请锁去判断资源是否就绪,而是等待通知
  • 当资源就绪时,某个东西来通知等待中的进程即可
  • 而实现这一过程,需要用到条件变量

概念

条件变量 

用于在线程之间建立一种通信机制,允许线程在某个条件不满足时等待,当条件满足时再继续执行

线程同步

确保多个线程之间的操作按照某种有序的方式执行,以避免竞态条件和确保程序的正确性

竞态条件

  • 在多线程环境下,程序的执行结果依赖于调度器调取线程的顺序,而这种结果是不确定的
  • 当多个线程同时访问和修改共享资源时,如果没有适当的同步机制,就可能导致竞态条件(也就是因为时序问题导致的程序异常)
  • 竞态条件可能引发各种问题,包括数据不一致、死锁等

条件变量接口

返回值

这里的接口都在pthread库中,而库中函数如果返回值是int,一般都是:

  • 成功,返回0
  • 失败,返回错误码

 

初始化

pthread_cond_init()

和互斥量的初始化是一样的

函数原型

饥饿问题,线程同步/竞态条件概念,条件变量的引入,概念,接口(初始化,销毁,等待,唤醒),示例代码(如何封装入口函数,唤醒的2种方式,访问临界资源(为什么需要锁))_第2张图片

cond

传入条件变量类型的指针作为输出型参数

attr

条件变量的属性

一般设置为nullptr即可,表示使用默认属性

pthread_cond_destroy()

同样也需要搭配destroy函数来手动销毁

PTHREAD_COND_INITIALIZER

  • 使用宏来初始化一个全局条件变量
  • 不需要手动销毁

 

等待条件满足

pthread_cond_wait()

函数原型

饥饿问题,线程同步/竞态条件概念,条件变量的引入,概念,接口(初始化,销毁,等待,唤醒),示例代码(如何封装入口函数,唤醒的2种方式,访问临界资源(为什么需要锁))_第3张图片

这里有两个函数,都可以用来让线程等待某个条件变量

  • 其中,timewait可以设置超时时间,如果在规定的时间内没有收到通知,则函数返回,这里我们不使用它
  • 我们主要来介绍wait
cond

指定线程等待某个条件变量

mutex 

这里是需要传入一个互斥量

互斥量哪里来呢?

  • 想想我们为什么需要用到条件变量
  • 是为了让线程在临界资源未就绪的情况下等待通知,而不是频繁去确认
  • 所以,条件变量的使用一定是在锁范围内的
  • 自然也就有对应的锁变量可以作为参数传入进去

为什么要传入一个锁呢?

  • 在后面的代码中我们再来讨论

 

唤醒线程

都是传入一个条件变量的地址作为参数,但功能的细节不同

pthread_cond_broadcast

函数原型

所有等待cond条件变量的线程,都发送资源就绪的通知(也就是将它们唤醒)

pthread_cond_signal

函数原型

通知等待队列中的一个线程

而具体通知哪一个线程,依赖于底层的调度策略

示例代码

让多个线程执行不同任务

代码
#include 
#include 
#include 
#include 

using namespace std;

#define TH_NUM 3
typedef void *(*func_t)(void *);

void *func1(void *args)
{
    string name = *((string *)args);
    cout << "im " << name << " , "
         << "work 1 ... " << endl;
    return nullptr;
}
void *func2(void *args)
{
    string name = *((string *)args);
    cout << "im " << name << " , "
         << "work 2 ... " << endl;
    return nullptr;
}
void *func3(void *args)
{
    string name = *((string *)args);
    cout << "im " << name << " , "
         << "work 3 ... " << endl;
    return nullptr;
}

void test1()
{
    func_t funcs[TH_NUM] = {func1, func2, func3}; // 存放线程任务
    vector tids;                       // 存放所有线程的tid

    for (int i = 0; i < TH_NUM; ++i)
    {
        string name = "pthread";
        name += to_string(i + 1);
        pthread_t tid;
        pthread_create(&tid, nullptr, funcs[i], (void *)&name);
        tids.push_back(tid);
    }
    for (auto tid : tids)
    {
        pthread_join(tid, nullptr);
    }
    cout << "join success" << endl;
}
介绍

这里我们采用数组来存放函数指针,然后派发给不同线程

封装统一入口函数

代码
#define TH_NUM 3
typedef void *(*func_t)(const string& name);

struct Data
{
    Data(const string &name, func_t func) : _name(name), _func(func) {}
    string _name;
    func_t _func;
};

void *func1(const string& name)
{
    cout << "im " << name << " , "
         << "work 1 ... " << endl;
    return nullptr;
}
void *func2(const string& name)
{
    cout << "im " << name << " , "
         << "work 2 ... " << endl;
    return nullptr;
}
void *func3(const string& name)
{
    cout << "im " << name << " , "
         << "work 3 ... " << endl;
    return nullptr;
}

void *entry(void *args)
{
    Data *pd = (Data *)args;
    pd->_func(pd->_name); // 将data中的成员作为另一成员的参数
}

void test1()
{
    func_t funcs[TH_NUM] = {func1, func2, func3}; // 存放线程任务
    vector tids;                       // 存放所有线程的tid

    for (int i = 0; i < TH_NUM; ++i)
    {
        string arr = "pthread";
        arr += to_string(i + 1);
        Data *pdata = new Data(arr, funcs[i]); // 在这里为线程分配任务

        pthread_t tid;
        pthread_create(&tid, nullptr, entry, (void *)pdata); // 所有线程都可以使用统一的入口函数entry,但执行的是不同的任务
        tids.push_back(tid);
    }
    for (auto tid : tids)
    {
        pthread_join(tid, nullptr);
    }
    cout << "join success" << endl;
}
介绍

这里我们用entry作为每个线程任务函数的入口

  • 采取在内部调用某个函数,让线程去执行的方式,实现不同线程不同任务
  • 那么这个函数就可以不受类型限制了,也就不用强转了
  • 并且,我们将线程要用到的变量+任务函数,全部封装在一个类中,然后传入entry即可

 

熟悉了一下多线程的使用,接下来就开始正题,来使用我们介绍的条件变量 :

唤醒等待同一条件变量的线程(两种方式)

引入

我们多线程的调度顺序由调度器决定,但如果我们想要自行控制哪个线程执行该怎么办呢?

这里就可以使用我们的条件变量了:

  • 先让所有线程都陷入等待状态
  • 之后,由主线程依次唤醒线程,而这个唤醒顺序就是陷入等待的顺序
  • 我们可以通过控制陷入等待的顺序,来控制线程执行的顺序
代码 
唤醒全部线程
#include 
#include 
#include 
#include 
#include 
#include 

using namespace std;

#define TH_NUM 3
typedef void *(*func_t)(const string &name, pthread_mutex_t *pmutex, pthread_cond_t *pcond);

struct Data
{
    Data(const string &name, func_t func, pthread_mutex_t *pmutex, pthread_cond_t *pcond)
        : _name(name), _func(func), _pcond(pcond), _pmutex(pmutex) {}
    string _name;
    func_t _func;
    pthread_mutex_t *_pmutex;
    pthread_cond_t *_pcond;
};

void *func1(const string &name, pthread_mutex_t *pmutex, pthread_cond_t *pcond)
{
    while (true)
    {
        pthread_cond_wait(pcond, pmutex);
        cout << "im " << name << " , "
             << "work 1 ... " << endl;
    }
    return nullptr;
}
void *func2(const string &name, pthread_mutex_t *pmutex, pthread_cond_t *pcond)
{

    while (true)
    {
        usleep(10);
        pthread_cond_wait(pcond, pmutex);
        cout << "im " << name << " , "
             << "work 2 ... " << endl;
    }
    return nullptr;
}
void *func3(const string &name, pthread_mutex_t *pmutex, pthread_cond_t *pcond)
{
    while (true)
    {
        usleep(20);
        pthread_cond_wait(pcond, pmutex);
        cout << "im " << name << " , "
             << "work 3 ... " << endl;
    }
    return nullptr;
}

void *entry(void *args)
{
    Data *pd = (Data *)args;
    pd->_func(pd->_name, pd->_pmutex, pd->_pcond); // 将data中的成员作为另一成员的参数
}

void test1()
{
    func_t funcs[TH_NUM] = {func1, func2, func3}; // 存放线程任务
    vector tids;                       // 存放所有线程的tid

    // 初始化锁和条件变量
    pthread_cond_t cond;
    pthread_cond_init(&cond, nullptr);
    pthread_mutex_t mutex;
    pthread_mutex_init(&mutex, nullptr);

    for (int i = 0; i < TH_NUM; ++i)
    {
        string arr = "pthread";
        arr += to_string(i + 1);
        Data *pdata = new Data(arr, funcs[i], &mutex, &cond); // 在这里为线程分配任务

        pthread_t tid;
        pthread_create(&tid, nullptr, entry, (void *)pdata); // 所有线程都可以使用统一的入口函数entry,但执行的是不同的任务
        tids.push_back(tid);
    }
    // 最开始,线程首先都按照顺序处于等待状态
    sleep(3);
    // 3s后,唤醒所有线程
    int count = 3;
    while (true)
    {
        pthread_cond_broadcast(&cond);
        sleep(1);
    }

    for (auto tid : tids)
    {
        pthread_join(tid, nullptr);
    }
    cout << "join success" << endl;

    pthread_mutex_destroy(&mutex);
    pthread_cond_destroy(&cond);
}
唤醒单个线程 
 while (true)
    {
        pthread_cond_signal(&cond);
        sleep(1);
    }
介绍
  • 这里我们用[设置线程睡眠时间长短]的方式,手动设置线程陷入等待的顺序 -> 线程被唤醒的顺序 -> 线程执行的顺序
  • 主线程在3s后,每隔1s唤醒所有线程:
  • 饥饿问题,线程同步/竞态条件概念,条件变量的引入,概念,接口(初始化,销毁,等待,唤醒),示例代码(如何封装入口函数,唤醒的2种方式,访问临界资源(为什么需要锁))_第4张图片
  • 每隔1s唤醒单个线程:
  • 饥饿问题,线程同步/竞态条件概念,条件变量的引入,概念,接口(初始化,销毁,等待,唤醒),示例代码(如何封装入口函数,唤醒的2种方式,访问临界资源(为什么需要锁))_第5张图片

结果虽然看起来相同,但输出的效果并不同

  • 使用broadcast时,每隔1s三个线程都在执行
  • 使用signal时,每隔1s只执行一个线程
  • 但都是按照我们设定的123顺序执行的

临界资源达成某种条件,退出线程(卡住,无法退出)

代码
#include 
#include 
#include 
#include 
#include 
#include 

using namespace std;

#define TH_NUM 3
typedef void *(*func_t)(const string &name, pthread_mutex_t *pmutex, pthread_cond_t *pcond);
volatile bool quit = false; // 让编译器不做优化,每次访问都需要从内存中拿取数据

struct Data
{
    Data(const string &name, func_t func, pthread_mutex_t *pmutex, pthread_cond_t *pcond)
        : _name(name), _func(func), _pcond(pcond), _pmutex(pmutex) {}
    string _name;
    func_t _func;
    pthread_mutex_t *_pmutex;
    pthread_cond_t *_pcond;
};

void *func1(const string &name, pthread_mutex_t *pmutex, pthread_cond_t *pcond)
{
    while (!quit)
    {
        pthread_cond_wait(pcond, pmutex);
        cout << "im " << name << " , "
             << "work 1 ... " << endl;
    }
    cout << name << " quit" << endl;
    return nullptr;
}
void *func2(const string &name, pthread_mutex_t *pmutex, pthread_cond_t *pcond)
{
    while (!quit)
    {
        pthread_cond_wait(pcond, pmutex);
        cout << "im " << name << " , "
             << "work 2 ... " << endl;
    }
    cout << name << " quit" << endl;
    return nullptr;
}
void *func3(const string &name, pthread_mutex_t *pmutex, pthread_cond_t *pcond)
{
    while (!quit)
    {
        pthread_cond_wait(pcond, pmutex);
        cout << "im " << name << " , "
             << "work 3 ... " << endl;
    }
    cout << name << " quit" << endl;
    return nullptr;
}

void *entry(void *args)
{
    Data *pd = (Data *)args;
    pd->_func(pd->_name, pd->_pmutex, pd->_pcond); // 将data中的成员作为另一成员的参数
}

void test1()
{
    func_t funcs[TH_NUM] = {func1, func2, func3}; // 存放线程任务
    vector tids;                       // 存放所有线程的tid

    // 初始化锁和条件变量
    pthread_cond_t cond;
    pthread_cond_init(&cond, nullptr);
    pthread_mutex_t mutex;
    pthread_mutex_init(&mutex, nullptr);

    for (int i = 0; i < TH_NUM; ++i)
    {
        string arr = "pthread";
        arr += to_string(i + 1);
        Data *pdata = new Data(arr, funcs[i], &mutex, &cond); // 在这里为线程分配任务

        pthread_t tid;
        pthread_create(&tid, nullptr, entry, (void *)pdata); // 所有线程都可以使用统一的入口函数entry,但执行的是不同的任务
        tids.push_back(tid);
    }
    // 最开始,线程首先都按照顺序处于等待状态
    sleep(3);
    // 3s后,唤醒所有线程

    int count = 3; 
    while (true)
    {
       --count;
        if (!count)
        {
            quit = true; // 让所有线程退出
            cout << "quit true" << endl;
            pthread_cond_broadcast(&cond); //修改quit后,需要让线程醒来才能获取到quit
            break;
        }
        pthread_cond_broadcast(&cond); 
        sleep(1);
    }

    for (auto tid : tids)
    {
        pthread_join(tid, nullptr);
    }
    cout << "join success" << endl;

    pthread_mutex_destroy(&mutex);
    pthread_cond_destroy(&cond);
}

介绍
  • 我们在count减为0时,将quit这个全局变量改为true,这样线程在进行循环条件的判断时,就可以退出循环了
  • 但是,运行结果却只有我们的线程1退出了:
  • 饥饿问题,线程同步/竞态条件概念,条件变量的引入,概念,接口(初始化,销毁,等待,唤醒),示例代码(如何封装入口函数,唤醒的2种方式,访问临界资源(为什么需要锁))_第6张图片
  • 为什么呢?
  • 原因就在于,我们并没有加入锁
  • (还记得pthread_cond_wait中传入的锁变量吗,我们并没有用到它)

加入锁

代码
void *func1(const string &name, pthread_mutex_t *pmutex, pthread_cond_t *pcond)
{
    while (!quit)
    {
        pthread_mutex_lock(pmutex);
        pthread_cond_wait(pcond, pmutex);
        cout << "im " << name << " , "
             << "work 1 ... " << endl;
        pthread_mutex_unlock(pmutex);
    }
    cout << name << " quit" << endl;
    return nullptr;
}
void *func2(const string &name, pthread_mutex_t *pmutex, pthread_cond_t *pcond)
{
    while (!quit)
    {
        pthread_mutex_lock(pmutex);
        pthread_cond_wait(pcond, pmutex);
        cout << "im " << name << " , "
             << "work 2 ... " << endl;
        pthread_mutex_unlock(pmutex);
    }
    cout << name << " quit" << endl;
    return nullptr;
}
void *func3(const string &name, pthread_mutex_t *pmutex, pthread_cond_t *pcond)
{
    while (!quit)
    {
        pthread_mutex_lock(pmutex);
        pthread_cond_wait(pcond, pmutex);
        cout << "im " << name << " , "
             << "work 3 ... " << endl;
        pthread_mutex_unlock(pmutex);
    }
    cout << name << " quit" << endl;
    return nullptr;
}
介绍

加上锁之后,确实是退出成功了:

饥饿问题,线程同步/竞态条件概念,条件变量的引入,概念,接口(初始化,销毁,等待,唤醒),示例代码(如何封装入口函数,唤醒的2种方式,访问临界资源(为什么需要锁))_第7张图片

但是,究竟原理是什么呢?

  • 先回想一下,我们引入条件变量,是为了解决 -- 当临界资源没有就绪的时候,线程依然会频繁访问临界资源来确认其是否就绪 的问题
  • 因为它不确定到底什么时候会就绪
  • 但是这样的行为是不合理的
  • 所以,我们引入条件变量 -- 当资源未就绪时,线程是等待别人告诉它已经就绪的消息,而不是傻不愣登的自己去一直问
  • 所以,这个函数的应用场景就应该是在资源不就绪时,所以必然在锁的范围内
  • 也就必然会对传入的锁变量有一些处理
  • 但如果我们不加锁,它可能就会出现一些问题 -- 比如,线程无法按照我们预想正常退出的问题

你可能感兴趣的:(linux,开发语言,linux)