『Linux』第九讲:Linux多线程详解(五)_ 信号量

这篇具有很好参考价值的文章主要介绍了『Linux』第九讲:Linux多线程详解(五)_ 信号量。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。

「前言」文章是关于Linux多线程方面的知识,上一篇是 Linux多线程详解(四),今天这篇是 Linux多线程详解(五),内容大致是信号量,讲解下面开始!

「归属专栏」Linux系统编程

「主页链接」个人主页

「笔者」枫叶先生(fy)

「枫叶先生有点文青病」「每篇一句」

求其上,得其中;

求其中,得其下;

求其下,必败。
——《孙子兵法》

『Linux』第九讲:Linux多线程详解(五)_ 信号量

十、POSIX信号量

10.1 分析之前代码的不足

在进入话题正文之前先谈三个概念:串行、并行、并发

  • 并发:并发是指同时处理多个任务,并且这些任务可能会相互影响,需要协调和管理。例如,在一个多用户系统中,多个用户可能同时访问同一个资源,需要通过并发控制来避免冲突和竞争
  • 并行:并行是指同时执行多个任务,每个任务都在不同的处理器上执行,相互之间独立,不会相互影响。例如,在一个多处理器系统中,多个任务可以同时在不同的处理器上执行,提高系统的处理能力和效率
  • 串行:串行是指按照一定的顺序依次执行任务,每个任务必须在前一个任务完成后才能开始执行。在串行执行中,每个任务都是独占CPU资源的,直到该任务执行完毕后,才会释放CPU资源给下一个任务使用

下面进入正文,代码样例是上一篇的Linux多线程(四)的代码

『Linux』第九讲:Linux多线程详解(五)_ 信号量

  • 一个线程在操作临界资源的时候,一定是要满足临界资源的条件的
  • 可是,是否满足条件,我们是无法直接得知的(不能事先得知,在没有访问临界资源之前,是无法得知是否满足条件
  • 只能先进行加锁,再检测,再操作,再解锁。根据检测的结果决定线程下一步怎么走
  • 进行检测也是在访问临界资源
  • 比如上面的代码样例中,线程是无法知道是否满足生产条件的,线程必须申请到锁,进入临界区才能检测是否满足条件,根据检测的结果决定线程下一步怎么走

注意:只要我们对临界资源进行加锁,就默认了对这个资源进行整体使用

但实际情况可能存在:一份临界资源,允许被不同线程同时访问不同的区域(把一份共享资源分成许多份使用) ,可以进行这样操作就是信号量 

10.2 信号量概念

信号量主要用于线程同步和互斥的

POSIX信号量和SystemV信号量作用相同,都是用于同步和互斥操作,达到无冲突的访问共享资源目的, 但POSIX可以用于线程间同步和互斥

信号量:semaphore

信号量的本质:

信号量(信号灯)本质是一个计数器,是描述临界资源中资源数目的计数器,信号量能够更细粒度的对临界资源进行管理

信号量解释:

  • 以例子进行解释,比如一个电影院,这个电影院看作是临界资源,电影院里面的每一个座位看作是临界资源划分成的一个个子资源;
  • 我只有买了票,这个座位在一定时间内才是归属我的,即便我买了票不去,这个座位也是我的,反过来,你没买票,这个座位就一定不是你的。买的票一定对应着一个座位。
  • 同样道理,我想用某种资源的时候,可以对这种资源进行预订,就相当于买票,买了票我就已经预订了这个座位,我预订了这个资源,它在未来的某一时间一定属于我
  • 假设电影院的座位只有100个,你不能卖出第101张票吧,信号量就相当于这里的票一样,票卖完了就不允许再卖了,再卖就会发生 ‘座位冲突’,也就对应进程访问该资源会发生冲突
  • 线程想要访问某个临界资源的条件是:申请到信号量,申请到了信号量就相当于预订了临界资源的某一部分小资源,就允许该线程对该资源的访问
  • 将一块临界资源拆分成一个个的子资源,不同的线程可以对不同的子资源进行访问,从而实现并发(程序员编码保证不同的线程可以并发访问临界资源的不同区域)

而申请信号量的前提是:所有线程要看到一个同一个信号量,所以信号量本身就是临界资源

  • 既然信号量是临界资源,是临界资源就要保证自身的安全性。
  • 信号量的本质是一个计数器,计数器的操作只有 ++(递增)和 --(递减)
  • 进行 ++ 和 -- 操作就要保证信号量自身的安全性,所以这个计数器就不能只是单纯整型int,因为这样的 ++ 和 -- 操作不是原子性的
  • 所以Linux对信号量进行封装了一种类型 sem_t,这种类型可以保证 ++ 和 -- 操作是原子性的

只要申请到了信号量,在未来就一定可以拥有该临界资源的一部分。

申请信号量的本质:对临界资源中特定小块资源进行预订(申请信号量就是一种预订机制)

信号量的核心操作是 ++ 和 --(PV操作

  • 对信号量++,归还信号量资源(V操作)
  • 对信号量--,申请信号量资源(P操作)

信号量这种预订机制,就有可能我们在真正访问临界资源之前,我们就可以提前知道临界资源的使用情况

  • 只要申请信号量成功,临界资源就一定会有一部分资源是你的
  • 只要申请信号量失败,就说明条件不就绪(不满足),你只能进行等待
  • 所以,就不需要像互斥锁,只有加锁后进行判断才能知道临界资源的使用情况

提前知道临界资源的使用情况:类比上面的电影院(临界资源),有一场电影只有100张票(临界资源分成100个小资源),只卖了80张,每张电影票对应着一个唯一的座位,电影还没有开始我们就已经预知了该电影院资源(座位)的使用情况

10.3 POSIX信号量函数

初始化信号量

初始化信号量的函数叫做 sem_init,man 3 sem_init 查看

『Linux』第九讲:Linux多线程详解(五)_ 信号量

函数:sem_init

头文件:#include <semaphore.h>

函数原型:
         int sem_init(sem_t *sem, int pshared, unsigned int value);

参数:
    第一个参数sem:需要初始化的信号量
    第二个参数pshare:0表示线程间共享,非零表示进程间共享
    第三个参数value:信号量初始值(信号量初始值)

返回值:
    初始化信号量成功返回0,失败返回-1,错误码被设置

 销毁信号量

销毁信号量的函数叫做 sem_destroy, man 3 sem_destroy查看

『Linux』第九讲:Linux多线程详解(五)_ 信号量

函数:sem_destroy

头文件:#include <semaphore.h>

函数原型:
         int sem_destroy(sem_t *sem);

参数:
    参数sem:需要销毁的信号量

返回值:
    销毁信号量成功返回0,失败返回-1,错误码被设置

等待信号量(申请信号量)(P操作)

等待信号量的函数叫做 sem_wait,man 3 sem_wait查看

『Linux』第九讲:Linux多线程详解(五)_ 信号量

函数:sem_wait

头文件:#include <semaphore.h>

函数原型:
         int sem_wait(sem_t *sem);

参数:
    参数sem:需要等待的信号量

返回值:
    等待信号量成功返回0,信号量的值-1
    等待信号量失败返回-1,错误码被设置,信号量的值保持不变

 发布信号量(释放信号量)(V操作)

发布信号量的函数叫做 sem_post,man 3 sem_post查看

『Linux』第九讲:Linux多线程详解(五)_ 信号量

函数:sem_post

头文件:#include <semaphore.h>

函数原型:
         int sem_post(sem_t *sem);

参数:
    参数sem:需要发布的信号量

返回值:
    等待信号量成功返回0,信号量的值+1
    等待信号量失败返回-1,错误码被设置,信号量的值保持不变

注意:信号量为1时,说明临界资源是一整个整体使用的(信号量所描述的临界资源只有一份),提供互斥功能(此时信号量的作用基本等价于互斥锁),提供互斥功能的信号量也叫二元信号量

十一、基于环形队列的生产消费模型

11.1 环形队列

环形队列本质上是一个数组,环形队列是固定大小的,在数据结构学过,不解释

环形队列的判空判满问题:(1)计数器,(2)空出最后一个位置

『Linux』第九讲:Linux多线程详解(五)_ 信号量

11.2  环形队列的生产消费模型

以但单生产单消费为例,生产者和消费者在这两种情况下可能会访问同一个位置:(1)环形队列空的时候,(2)环形队列满的时候,对于其他情况生产者和消费者访问的位置都不一样

『Linux』第九讲:Linux多线程详解(五)_ 信号量

环形队列的生产消费模型,必须要满足两个条件:

  1. 消费者不能超过生产者
  2. 无论是生产者还是消费者,不允许将对方套一个圈以上

对于生产者和消费者来说,它们关注的资源是不同的:

  • 生产者关注的是环形队列当中是否有空间,只要有空间生产者就可以进行生产。
  • 消费者关注的是环形队列当中是否有数据,只要有数据消费者就可以进行消费。

这里就需要用信号量来描述环形队列当中的空间资源和数据资源,需要创建两个信号量,假设是A,B,A信号量用来标识空间资源的多少,B信号量用于标识数据资源的多少,并且这两个信号量初始化的值不同:

  • A信号量初始值我们应该设置为环形队列的容量,因为刚开始时环形队列当中全是空的,空间资源为环形队列的容量
  • B信号量初始值我们应该设置为0,因为刚开始时环形队列当中没有数据

生产者申请空间资源,释放数据资源

  • 对于生产者来说,生产者每次生产数据前都需要先申请(等待)A信号量,
  • 如果A信号量的值不为0,则信号量申请成功,A信号量-1(即等待A信号量成功,P操作),此时生产者可以进行生产操作。
  • 如果A信号量的值为0,则信号量申请失败,此时生产者需要在A信号量的等待队列下进行阻塞等待,直到环形队列当中有新的空间后再被唤醒

当生产者生产完数据后,应该释放B信号量,因为此时队列中的数据已经+1,B信号量也需要+1(即释放(发布)B信号量,V操作)

消费者申请数据资源,释放空间资源 

  • 对于消费者来说,消费者每次消费数据前都需要先申请(等待)B信号量,
  • 如果B信号量的值不为0,则信号量申请成功,B信号量-1(即等待A信号量成功,P操作),此时消费者可以进行消费操作。
  • 如果B信号量的值为0,则信号量申请失败,此时消费者需要在B信号量的等待队列下进行阻塞等待,直到环形队列当中有新的数据后再被唤醒

当消费者消费完数据后,应该释放A信号量,因为此时队列中的数据已经-1,空出一个空间资源,A信号量也需要+1(即释放(发布)A信号量,V操作)

下面进行编写代码 

11.3 代码实现

Linux多线程(四)实现的是是基于queue的生产者消费者模型,其空间可以动态分配(采用互斥锁+条件变量实现);现在基于固定大小的环形队列重写这个生产者消费者模型(采用POSIX信号量实现)

为了方便理解,实现的代码也是单生产者,单消费者

RingQueue.hpp

用C++STL库当中的vector模拟实现环形队列 

#pragma once

#include <iostream>
#include <vector>
#include <semaphore.h>
#include <cassert>

static const int gcap = 5;

template <class T>
class RingQueue
{
private:
    // P操作,等待信号量
    void P(sem_t &sem)
    {
        int n = sem_wait(&sem);
        assert(n == 0);
        (void)n;
    }
    // V操作,发布信号量
    void V(sem_t &sem)
    {
        int n = sem_post(&sem);
        assert(n == 0);
        (void)n;
    }

public:
    RingQueue(const int &cap = gcap) : _ringQueue(cap), _cap(cap)
    {
        sem_init(&_spaceSem, 0, _cap);
        sem_init(&_dataSem, 0, 0);
        _productorStep = _consumerStep = 0;
    }
    // 生产者生产数据
    void Push(const T &in)
    {
        // 申请信号量,申请成功继续执行代码,失败阻塞等待
        P(_spaceSem);
        _ringQueue[_productorStep] = in;
        _productorStep++;
        _productorStep %= _cap;
        // 此时数据+1,发布_dataSem信号量
        V(_dataSem);
    }
    // 消费者消费数据
    void Pop(T *out)
    {
        // 申请信号量,申请成功继续执行代码,失败阻塞等待
        P(_dataSem);
        *out = _ringQueue[_consumerStep];
        _consumerStep++;
        _consumerStep %= _cap;
        // 此时数据-1,发布_spaceSem信号量
        V(_spaceSem);
    }

    ~RingQueue()
    {
        sem_destroy(&_spaceSem);
        sem_destroy(&_dataSem);
    }

private:
    std::vector<T> _ringQueue;
    int _cap;
    sem_t _spaceSem;    // 空间资源
    sem_t _dataSem;     // 数据资源
    int _productorStep; // 生产者的下标,共用_ringQueue,两套下标
    int _consumerStep;  // 消费者的下标,共用_ringQueue
};

Main.cc 

主函数只需要创建一个生产者线程和一个消费者线程,生产者线程不断生产数据放入环形队列,消费者线程不断从环形队列里取出数据进行消费 

#include "RingQueue.hpp"
#include <pthread.h>
#include <unistd.h>
#include <ctime>

using namespace std;

void *consumer(void *ringqueue)
{
    RingQueue<int> *rq = static_cast<RingQueue<int> *>(ringqueue);
    while (true)
    {
        int data;
        rq->Pop(&data);
        cout << "消费完成,消费的数据是:" << data << endl;
        sleep(1);
    }
    return nullptr;
}

void *productor(void *ringqueue)
{
    RingQueue<int> *rq = static_cast<RingQueue<int> *>(ringqueue);
    while (true)
    {
        int data = rand() % 10 + 1;
        rq->Push(data);
        cout << "生产完成,生产的数据是:" << data << endl;
        sleep(1);
    }
    return nullptr;
}

int main()
{
    srand((unsigned int)time(nullptr) ^ 0x4589315); // 用于获取随机数

    RingQueue<int> *rq = new RingQueue<int>();
    // c:consumer p:productor
    pthread_t c, p;
    pthread_create(&c, nullptr, consumer, rq);
    pthread_create(&p, nullptr, productor, rq);

    // 线程等待
    pthread_join(c, nullptr);
    pthread_join(p, nullptr);
    delete rq;
    return 0;
}

 生产者消费者步调一致

生产者每隔一秒生产一个数据,而消费者是每隔一秒消费一个数据,因此运行代码后我们可以看到生产者和消费者的执行步调是一致的

测试运行结果

『Linux』第九讲:Linux多线程详解(五)_ 信号量

生产者生产快,消费者消费慢

修改代码,生产者生产快,消费者消费慢

『Linux』第九讲:Linux多线程详解(五)_ 信号量

测试运行结果

  • 第一条打印是因为两个线程同时执行,消费者线程消费了一次数据才sleep(1)
  • 此时由于生产者生产的很快,运行代码后一瞬间生产者就将环形队列打满了,此时生产者想要再进行生产,但空间资源已经为0了,生产者只能阻塞等待空间资源
  • 消费者消费一个环形队列里面的数据,生产者又会生产一个数据,因此后续生产者和消费者的步调又变成一致的了

 『Linux』第九讲:Linux多线程详解(五)_ 信号量

生产者生产慢,消费者消费快

修改代码,生产者生产慢,消费者消费快

『Linux』第九讲:Linux多线程详解(五)_ 信号量

测试运行

  • 第一条打印是因为两个线程同时执行,生产者线程生产了一个数据才sleep(1)
  • 此时由于消费者消费的很快,运行代码由于环形队列中没有数据,只能阻塞等待生产者生产数据
  • 生产者生产一个数据,消费者又会消费一个数据,因此后续生产者和消费者的步调又变成一致的了

『Linux』第九讲:Linux多线程详解(五)_ 信号量

--------------------- END ----------------------文章来源地址https://www.toymoban.com/news/detail-471622.html

「 作者 」 枫叶先生
「 更新 」 2023.6.3
「 声明 」 余之才疏学浅,故所撰文疏漏难免,
          或有谬误或不准确之处,敬请读者批评指正。

到了这里,关于『Linux』第九讲:Linux多线程详解(五)_ 信号量的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • 【Linux学习】多线程——信号量 | 基于环形队列的生产者消费者模型 | 自旋锁 | 读写锁

    🐱作者:一只大喵咪1201 🐱专栏:《Linux学习》 🔥格言: 你只管努力,剩下的交给时间! 之前在学习进程间通信的时候,本喵简单的介绍过一下信号量,今天在这里进行详细的介绍。 这是之前写的基于阻塞队列的生产者消费者模型中向阻塞队列中push任务的代码。 上面代码

    2024年02月07日
    浏览(43)
  • 『Linux』第九讲:Linux多线程详解(二)_ 线程控制

    「前言」文章是关于Linux多线程方面的知识,上一篇是 Linux多线程详解(一),今天这篇是 Linux多线程详解(二),讲解会比较细,下面开始! 「归属专栏」Linux系统编程 「主页链接」个人主页 「笔者」枫叶先生(fy) 「枫叶先生有点文青病」「每篇一句」 纵有千古,横有八荒

    2024年02月01日
    浏览(30)
  • 『Linux』第九讲:Linux多线程详解(三)_ 线程互斥 | 线程同步

    「前言」文章是关于Linux多线程方面的知识,上一篇是 Linux多线程详解(二),今天这篇是 Linux多线程详解(三),内容大致是线程互斥与线程同步,讲解下面开始! 「归属专栏」Linux系统编程 「主页链接」个人主页 「笔者」枫叶先生(fy) 「枫叶先生有点文青病」「每篇一句

    2024年02月02日
    浏览(62)
  • 并发编程 --- 信号量线程同步

    上文编码技巧 --- 同步锁对象的选定中,提到了在C#中,让线程同步有两种方式: 锁(lock、Monitor等) 信号量(EventWaitHandle、Semaphore、Mutex) 加锁是最常用的线程同步的方法,就不再讨论,本篇主要讨论使用信号量同步线程。 实际上,再C#中 EventWaitHandle 、 Semaphore 、 Mutex 都是

    2024年02月16日
    浏览(32)
  • 线程同步-信号量-互斥量-条件变量

    线程同步其实实现的是线程排队。 防止线程同步访问共享资源造成冲突。 多个线程访问共享资源的代码有可能是同一份代码,也有可能是不同的代码;无论是否执行同一份代码,只要这些线程的代码访问同一份可变的共享资源,这些线程之间就需要同步。 1. 问题 同一个进程

    2023年04月16日
    浏览(28)
  • C多线程、锁、同步、信号量

    1.1 创建线程 thread 是线程变量地址 attr是线程属性,一般为NULL start_rount 是函数指针 arg 是函数指针指向函数的参数 1.2 线程退出 retval可以把退出值带回去,例子见线程回收 1.3 线程回收 1.4 线程分离: 某些情况下,程序的主线程有自己的其他业务,如果让主线程负责子线程的

    2024年02月02日
    浏览(62)
  • iOS信号量造成线程优先级反转

    在并发队列使用信号量会可能会造成线程优先级反转 经过查询资料,发现是在XCode14上增加了工具,比如 : Thread Performance Checker (XCode14上默认开启的),这个工具会让APP在运行的时候,发现有例如线程优先级反转和非UI工作在主线程上运行等问题的时候,就会在XCode问题导航

    2024年02月05日
    浏览(27)
  • 【Linux】详解进程通信中信号量的本质&&同步和互斥的概念&&临界资源和临界区的概念

             访问资源在安全的前提下,具有一定的顺序性,就叫做同步 。在多道程序系统中,由于资源有限,进程或线程之间可能产生冲突。同步机制就是为了解决这些冲突,保证进程或线程之间能够按照既定的顺序访问共享资源。同步机制有助于避免竞态条件和死锁(

    2024年04月25日
    浏览(25)
  • 线程同步、生产者消费模型和POSIX信号量

    gitee仓库: 1.阻塞队列代码:https://gitee.com/WangZihao64/linux/tree/master/BlockQueue 2.环形队列代码:https://gitee.com/WangZihao64/linux/tree/master/ringqueue 概念 : 利用线程间共享的全局变量进行同步的一种机制,主要包括两个动作:一个线程等待\\\"条件变量的条件成立\\\"而挂起;另一个线程使“

    2024年02月03日
    浏览(32)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包