初识linux之POSIX信号量

news2025/6/25 15:26:32

目录

一、信号量的概念

1. 信号量的作用

2. 信号量的PV操作

3. 信号量操作接口

3.1 初始化信号量

3.2 销毁信号量

3.3 等待信号量(P操作)

3.4 发布信号量(V操作)

二、循环队列

三、使用循环队列模拟实现生产消费模型

1. 注意事项

2. 循环队列文件

3. 测试文件

4. 任务文件

5. 循环队列的高效问题


一、信号量的概念

1. 信号量的作用

在以前,大家应该都了解过system V中的信号量。在这里,要讲的是POSIX信号量。它的作用和system V的信号量的作用是相同的,都是用于同步操作,达到无冲突的访问共享资源的目的。但是与system V信号量不同的是,POSIX信号量可以用于线程同步

例如在上篇文章“同步与生产消费模型”中所写的生产消费模型的代码中,缓冲区被锁给保护起来了,在这个区域内,同时只能有一个线程可以访问。这也就是说,只要我们对资源进行整体加锁,其实就是默认了对这份资源的整体使用。

但是也可能存在这么一种情况,虽然这份公共资源是被整体使用的,但它允许同时访问不同的区域。就好比电影院的放映厅可以看成一份公共资源,每个放映厅中同时只能放一个电影,但是它有用大量的座位,可以被多个观众买票观看。

因此,信号量的本质其实就是一个“计数器”,用于衡量临界资源中资源数量的多少。每个线程在访问公共资源之前,都要先去申请一个信号量,只有申请到了信号量的线程才可以继续向下执行代码,否则只能阻塞等待。这就好比我们去电影院买票,只有买了票的观众才能进入放映厅,没有买到票的只能在外面等待电影结束。

只要拥有了信号量,那么这个线程在未来的某段时间内拥有了临界资源的一部分。因此,申请信号量的本质,就是对临界资源中特定小块资源的“预订”机制。多线程下的不同线程访问同一份公共资源的不同区域的行为,就需要由程序员自行编码实现,信号量只提供将公共资源划分为不同区域的功能。即电影院中的售票功能由信号量提供,而座位安排则有工作人员自行决定。

在之前我们写的生产消费模型的代码中,为了控制线程串行访问临界资源,需要通过循环的方式加锁以适应多线程的访问。其本质是因为我们不知道临界资源的使用情况,所以只能通过锁是否被释放来判断临界资源这一整体是否被占用。有了信号量之后,我们就可以明确知道临界资源的资源块数量,通过资源块数量来了解临界资源的使用情况,进而更好的分配临界资源,而无需再像之前那样循环判断锁的状态。

2. 信号量的PV操作

线程要访问临界资源之前,就要先申请信号量。而我们知道,每个线程内都有一个单独的栈结构,保存了自己的函数体。因此,要让多个线程看到同一个信号量,就必须保证信号量也是公共资源。既然信号量是公共资源,就说明信号量也需要被保护起来,以免线程安全问题

信号量是一个计数器,这就说明信号量其实就可以看做一个数字。当有线程申请信号量时,信号量就需要--;反之线程释放信号量时,就需要++。因此,保证信号量安全的方式,就是让信号量的++和--操作是原子的

在信号量中,对计数器--,即申请信号量的操作,被叫做“P操作”;对计数器++,即释放信号量的操作,被叫做“V操作”。信号量的核心操作,就是“PV操作”,也叫做“PV原语”

3. 信号量操作接口

3.1 初始化信号量

信号量其实也是一种数据类型。所以,要使用信号量,首先就要用“sem_t”定义出一个信号量。比如“sem_t sem”。这一点和锁、条件变量都是一样的。

当信号量被定义出来后,就需要进行初始化。信号量的初始化函数是sem_init

第一个参数sem为要初始化的信号量;第二个参数pshared表示共享方式0为线程间共享非0为进程间共享。一般在使用时填为0即可;第三个参数是信号量的初始值,取决于共享资源中资源数量的多少。

3.2 销毁信号量

当信号量不需要使用时,使用sem_destroy销毁即可:

3.3 等待信号量(P操作)

当有线程来申请信号量时,可以使用sem_wait函数,该函数会将信号量-1:

3.4 发布信号量(V操作)

当有线程使用完资源,需要归还信号量时,就可以使用“sem_post”函数,该函数会将信号量+1:

二、循环队列

在上一篇文章“线程同步与生产消费模型”中,我们用“阻塞队列”模拟实现了一个简单的生产消费模型。在阻塞队列中,每次只能有一个生成线程或消费线程访问缓冲区。但是,里面却存有多份资源。很显然,在这种可以将一份公共资源——缓冲区划分为多个小块的情况下,阻塞队列的效率就比较低。既然如此,我们能不能设计另一种访问方式,支持多个线程访问这份公共资源中的不同部分的结构呢?循环队列就可以实现这一目标。

环形队列,可以看成一个首尾相连的数组:

在环形队列中,同时会有两个线程存在,即消费线程和生产线程。在一开始的时候,消费线程和生产线程在同一起始位置。向后运行时,消费线程和生产线程只有两种情况会在同一位置,即环形队列为空或为满时。在其他情况下,消费线程和生产线程所访问的都是同一份资源的不同区域。

这样看可能不太好理解,举个例子。现在有A和B两个人,在它们两个人面前有一张桌子,这个桌子上摆了10个盘子。现在A和B要玩一个放苹果的游戏。游戏开始前,A和B在同一位置站着。游戏开始后,A沿着桌子走,往盘子里面放苹果;B跟在A后面,从盘子里面拿苹果。在这个游戏里面有两个规则:放苹果的人不能超过拿苹果的人拿苹果的人也不能超过放苹果的人

通过上面的规则限制,就保证了A永远在B的前面。当A和B相遇时,只有两种情况。一种是A放苹果的速度太快,套了B一个圈,A就只能站在B旁边等B拿完苹果后往空盘子上放苹果;第二种就是B拿苹果的速度太快,直接跟到了A的身后,A放一个苹果,B就拿一个。这两种情况分别就对应了整张桌子上苹果的满和空两种状态。

在上面的例子里面,A就是生产线程,B就是消费线程。A往空盘子里面放苹果的行为,就是生产线程向循环队列传输数据B从盘子里拿苹果的行为,就是消费线程从循环队列里面拿取数据

要注意,虽然这个结构的名字是循环队列,但是它的本质上是一个数组。因为消费线程和生产线程要通过数组的下标来传输拿取数据和判断它们是否遵守了不能超过对方的规则。然后通过取模的方式让消费线程和生产线程在数组中循环访问。

那么在这个结构里,信号量的作用是什么呢?其实就是用来衡量循环队列中资源数量的多少。

这里的资源对生产线程和消费线程来讲,是不同的。

对于生产线程而言,这里的资源指的是循环队列中可存放数据的空间。对于消费线程而言,这里的资源指的是循环队列中可拿取资源的数量。因此,可以通过分别给消费线程和生产线程定义一个信号量的方式,让它们提前知道资源的使用情况。

通过上面的讲解,大家应该也都知道了,循环队列相比阻塞队列将资源视为一个整体,循环队列中的资源被划分为了多个小块,在大部分情况下,单生产单消费都是可以并发运行的,只有在满和空时才会有互斥与同步的关系。

三、使用循环队列模拟实现生产消费模型

1. 注意事项

首先要知道,对于生产者和消费者而言,生产者和消费者的信号量代表不同的资源。

生产者的资源是循环队列中的空位个数,所以初始值应为循环队列的可放入数据空间个数。当生产者申请成功信号量,即执行P操作时,表明该线程可以继续向下运行;申请失败,则阻塞在申请信号的位置。当生产执行完将数据放入到循环队列中的任务后,就需要释放信号量,即执行V操作。但是要注意,这里生产者的资源是可放入资源的空位,所以在生产者放入资源后,并不是对生产者的信号量做V操作,而是对消费者的信号量做V操作,++消费者的信号量,告知消费者此时有资源可以消费。生产者自身的信号量保持不变。

消费者的资源是可拿取资源的个数,所以初始值应为0。当线程进入时,首先会遇到信号量为0的情况,此时就需要等待生产者生产数据并++消费者的信号量。当线程申请到信号量后,就可以向下运行。拿取数据后,消费者就要释放信号量,执行V操作。同样的,这里的消费者也是释放生产者的信号量,++生产者的信号量,告诉生产者此时又有空位可以放入数据了。

那么如何控制生产线程和消费线程的位置呢?就是通过数组下标来控制。当为空或为满时,生产线程和消费线程的下标相同,此时就说明该位置同时一定只能有一个线程访问,需要维护互斥与同步。在其他情况下,因为已经用信号量保证了生产线程和消费线程一定不会超过对方,所以其他情况下生产线程和消费线程访问的就是不同区域,支持并发。

2. 循环队列文件

#pragma once
#include<iostream>
#include<vector>
#include<semaphore.h>
#include<cassert>
#include<pthread.h>

static const int gcap = 5;//环形队列中的数据个数

template<class T>
class RingQueue
{
public:
    RingQueue(const int& cap = gcap)
    :_queue(cap), _cap(cap)
    {
        int n = sem_init(&_spaceSem, 0, _cap);//初始化生产者的信号量
        assert(n == 0);
        n = sem_init(&_dataSem, 0, 0);//初始化消费者的信号量
        assert(n == 0);//返回0说明信号量初始化成功
        (void)n;

        _ProductorStep = _ConsumerStep = 0;//从0下标开始访问

        pthread_mutex_init(&_pmutex, nullptr);
        pthread_mutex_init(&_cmutex, nullptr);//初始化锁
    }

    void P(sem_t& sem)//p操作,申请信号量
    {
        int n = sem_wait(&sem);
        assert(n == 0);
        (void)n;
    }

    void V(sem_t& sem)//v操作,释放信号量
    {
        int n = sem_post(&sem);
        assert(n == 0);
        (void)n;
    }

    void Push(const T& in)//向唤醒队列发送数据
    {
        P(_spaceSem);//生产者申请信号量

        pthread_mutex_lock(&_pmutex);//加锁

        _queue[_ProductorStep++] = in;//将数据放入循环队列
        _ProductorStep %= _cap;//取模,保证下标一直位于数组中
        V(_dataSem);//释放消费者的信号量,告知消费者有数据可消费

        pthread_mutex_unlock(&_pmutex);//解锁
    }

    void Pop(T* out)//从环形队列接收数据
    {
        P(_dataSem);//消费者申请信号量

        pthread_mutex_lock(&_cmutex);//加锁

        *out = _queue[_ConsumerStep++];//消费者获取数据
        _ConsumerStep %= _cap;//取模,保证下标位于数组中
        V(_spaceSem);//释放生产者的信号量,告知生产者有空位可存放数据

        pthread_mutex_unlock(&_cmutex);//解锁
    }

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

        pthread_mutex_destroy(&_pmutex);
        pthread_mutex_destroy(&_cmutex);
    }

private:
    std::vector<T> _queue;//环形队列
    int _cap;//队列中的数据个数
    sem_t _spaceSem;//生产者的信号量(空间资源)
    sem_t _dataSem;//消费者的信号量(数据资源)
    int _ProductorStep;//生产者的位置
    int _ConsumerStep;//消费者的位置
    pthread_mutex_t _pmutex;//生产者的锁
    pthread_mutex_t _cmutex;//消费者的锁 
};

3. 测试文件

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

const int pNum = 4;
const int cNum = 8;

std::string selfName()
{
    char name[64];
    snprintf(name, sizeof(name), "thread[0x%x]", pthread_self());

    return name;
}

void* ProductorRoutine(void* args)//生产工作
{
    RingQueue<CalTask>* rq = static_cast<RingQueue<CalTask>*>(args);//获取工作

    while(true)
    {
        //执行生产活动
        int x = rand() % 10 + 1;
        int y = rand() % 20 + 1;
        char ch = oper[rand() % oper.size()];
        CalTask cal(x, y, ch, mymath);

        rq->Push(cal);//派发任务
        std::cout << selfName() << "生产者派发了一个任务: " << cal.tostring() << std::endl;

        sleep(1);
    }
}

void* ConsumerRoutine(void* args)//消费工作
{
    RingQueue<CalTask>* rq = static_cast<RingQueue<CalTask>*>(args);//获取工作

    while(true)
    {
        //执行消费活动
        CalTask cal;
        rq->Pop(&cal);
        std::string result = cal();
        std::cout << selfName() << "消费者消费了一个任务: " << result << std::endl;
    }
}

int main()
{
    srand(time(0));//生成随机数种子 
    RingQueue<CalTask>* rq = new RingQueue<CalTask>;
    pthread_t p[pNum], c[cNum];
    for(int i = 0; i < pNum; ++i) pthread_create(p + i, nullptr, ProductorRoutine, (void*)rq);
    for(int i = 0; i < cNum; ++i) pthread_create(c + i, nullptr, ConsumerRoutine, (void*)rq);
    
    for(int i = 0; i < pNum; ++i) pthread_join(p[i], nullptr); 
    for(int i = 0; i < pNum; ++i) pthread_join(c[i], nullptr);

    delete rq;
    return 0;
}

4. 任务文件

#include<iostream>
#include<functional>
#include<string>
#include<stdio.h>

class CalTask//计算任务
{
    typedef std::function<int(int, int, char)> func_t;//函数指针
public:
    CalTask()
    {}

    CalTask(const int& x, const int& y, const char& op, func_t func)
    :_x(x), _y(y), _op(op), _func(func)
    {}

    std::string operator()()
    {
        int result = _func(_x, _y, _op);//外部传入的计算函数

        char buffer[64];
        snprintf(buffer, sizeof(buffer), "%d %c %d = %d", _x, _op, _y, result);

        return buffer;
    }

    std::string tostring()
    {
        char buffer[64];
        snprintf(buffer, sizeof(buffer), "%d %c %d = ?", _x, _op, _y);

        return buffer;
    }

private:
    int _x;
    int _y;
    char _op;
    func_t _func;
};

std::string oper = "+-*/%";//提供五种计算方法

int mymath(int x, int y, char op)//计算方法
{
    int result = 0;
    switch(op)
    {
        case '+':
        {
            result = x + y;
            break;
        }
        case '-':
        {
            result = x - y;
            break;
        }
        case '*':
        {
            result = x * y;
            break;
        }
        case '/':
        {
            if(y == 0)
            {
                std::cout << "div zero error!" << std::endl;
                result = -1;
            }
            else
                result = x / y;    
            
            break;
        }
        case '%':
        {
            if(y == 0)
            {
                std::cout << "mod zero error" << std::endl;
                result = -1;
            }
            else
                result = x % y;

            break;           
        }
        default:
            break;
    }

    return result;
}

在上面的循环队列中,有几个注意事项。首先是在循环队列中的push和pop函数中,加锁的位置要在申请信号量后面,这样才能保证线程进来后先让信号量判断是否有内容可读,要不要向后运行。第二个就是,因为是要通过下标来判断生产线程和消费线程的使用的资源,所以最好提供两个变量,标识消费线程和生产线程的位置。

5. 循环队列的高效问题

在循环队列中,虽然支持生产线程和消费线程并发运行,但是在同一时刻,只允许一个生产线程和一个消费线程生产拿取数据。在多生产多消费的情况下,依然可以看做串行运行。因此,使用循环队列的生产消费模型的高效并不是体现在传入和拿取任务的时候,而是支持多个生产线程并发拿取数据生成任务和多个消费线程并发执行计算任务上,这一点和使用阻塞队列的生产消费模型是一样的。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/520193.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Spring 组成及拓展

1. Spring 组成 1.1 Spring的七大模块 1.2拓展 在Spring官网有这个介绍&#xff1a;现代化的Java开发&#xff01;说白了就是基于Spring的开发 - SpringBoot - 一个快速开发的脚手架 - 基于SpringBoot可以快速的开发单个微服务。 - 约定大于配置&#xff01; - SpringC…

ENVI实现遥感图像的最小距离、最大似然、支持向量机分类

目录 1 分类需求 2 具体操作 2.1 ROI区域绘制 2.2 最小距离法 2.3 最大似然法 2.4 支持向量机 3 精度评定 4 分类后处理 4.1 小斑块处理 4.2 分类统计 4.3 修改类别颜色 5 结果对比 本文介绍基于ENVI软件&#xff0c;实现最小距离法、最大似然法与支持向量机三种遥…

达索的多领域系统级仿真软件Dymola 2023版本下载与安装配置教程

目录 前言一、Dymola 安装二、使用配置总结 前言 Dymola是由Dassault Systemes公司开发的一款基于物理建模的多领域系统级仿真软件。它包含了多个领域的建模和仿真工具&#xff0c;如机械、电气、液压、热力学、控制等&#xff0c;可以用于对各种系统进行建模和仿真&#xff0…

GcExcel for Java edition 6.1.0 Crack

高速 Java Excel 电子表格 API 库,在 Java 应用程序中以编程方式创建、编辑、导入和导出 Excel 电子表格。几乎可以在任何地方部署。 创建、加载、编辑和保存 Excel 电子表格 保存为 .XLSX、PDF、HTML、CSV 和 JSON 基于具有零 Excel 依赖性的 Excel 对象模型 在本地、内部或云…

利用Facebook群组和页面打造忠实粉丝基础

社交媒体的崛起改变了我们与世界互动的方式&#xff0c;而Facebook作为其中的佼佼者&#xff0c;不仅让我们能够与朋友、家人保持联系&#xff0c;还提供了许多机会用于商业营销。利用Facebook群组和页面来打造忠实粉丝基础是许多品牌成功的关键之一。 一、创建一个引人注目的F…

bat脚本

bat脚本 bat脚本就是DOS批处理脚本&#xff0c;就是将一系列DOS命令按照一定顺序排列而形成的集合&#xff0c;运行在windows命令行环境上。这个文件的每一行都是一条DOS命令 在命令提示下键入批处理文件的名称&#xff0c;或者双击该批处理文件&#xff0c;系统就会调用Cmd.…

Java每日一练(20230513) 输出最值、盛水容器、旋转数组II

目录 1. 输出最值 ※ 2. 盛最多水的容器 &#x1f31f;&#x1f31f; 3. 搜索旋转排序数组 II &#x1f31f;&#x1f31f; &#x1f31f; 每日一练刷题专栏 &#x1f31f; Golang每日一练 专栏 Python每日一练 专栏 C/C每日一练 专栏 Java每日一练 专栏 1. 输出最值…

深度学习环境配置系列文章(二):Anaconda配置Python和PyTorch

深度学习环境配置系列文章目录 第一章 专业名称和配置方案介绍 第二章 Anaconda配置Python和PyTorch 第三章 配置VS Code和Jupyter的Python环境 第四章 配置Windows11和Linux双系统 第五章 配置Docker深度学习开发环境 第二章文章目录 深度学习环境配置系列文章目录前言一&…

VMware常用操作

一、vsphere linux虚拟机在线添加磁盘 1、编辑虚拟机设置 添加硬盘 后面步骤下一步即可 然后ssh 到虚拟机 查看/sys/class/scsi_host/ 有几个hostx按下列方法在线认磁盘 echo "- - -" > /sys/class/scsi_host/host2/scan 1、认到磁盘后创建pv pvcreate /dev/sdb …

【C++ 入坑指南】(04)基础语法

文章目录 一、注释二、变量三、常量四、关键字五、标识符 一、注释 作用&#xff1a;在代码中加一些说明和解释&#xff0c;方便自己或其他程序员阅读代码。 C 中有两种注释&#xff1a; 单行注释&#xff1a; // 描述信息 通常放在一行代码的上方&#xff0c;或者一条语句的…

key_vector详解

key_vector是linux网络路由时&#xff0c;非常重要的一个结构&#xff0c;其定义如下&#xff1a; struct key_vector { t_key key; unsigned char pos; /* 2log(KEYLENGTH) bits needed */ unsigned char bits; /* 2log(KEYLENGTH) bits needed */ unsigned char slen; 子网长…

kubernetes❀资源管理

kubernetes❀资源管理 3. 资源管理3.1 资源管理介绍3.2 YAML语言介绍3.3 资源管理方式3.3.1 命令式对象管理3.3.2 命令式对象配置3.3.3 声明式对象配置 3. 资源管理 3.1 资源管理介绍 在kubernetes中&#xff0c;所有的内容都抽象为资源&#xff0c;用户需要通过操作资源来管…

Git advanced高级操作

这篇文章是继Git概念介绍&#xff0c;常用命令与工作流程整理 配图_TranSad的博客-CSDN博客 之后的一些补充&#xff0c;学习总结一些额外Git操作中的比较常用的操作。所以这篇文章假设你已经有了前面的基础&#xff0c;我就直接说一些没有提到过的部分。 Detached HEAD 在G…

镜面反射BRDF模型(Specular BRDF)

利用这些假设&#xff08;局部光学平坦表面&#xff0c;没有相互反射&#xff09;&#xff0c;可以很容易推导出一个被称为Microfacet Cook-Torrance BRDF的一般形式的Specular BRDF项。此Specular BRDF具有以下形式&#xff1a; 其中&#xff1a; D(h) : 法线分布函数 &#…

数据结构与算法十一 图的入门

一 图的入门 1.1 图的实际应用 在现实生活中&#xff0c;有许多应用场景会包含很多点以及点点之间的连接&#xff0c;而这些应用场景我们都可以用即将要学习的图这种数据结构去解决。 地图&#xff1a; 我们生活中经常使用的地图&#xff0c;基本上是由城市以及连接城市的道…

【Nginx高级篇】nginx扩容

目录 一、单机垂直扩容&#xff1a;硬件资源增加 二、水平扩展&#xff1a;集群化 &#xff08;一&#xff09;会话管理 1、Nginx高级负载均衡 2、使用sticky模块完成对Nginx的负载均衡 3、keepalive &#xff08;二&#xff09;upstream的工作流程 &#xff08;三&…

为什么别人家的ChatGPT比我家的更聪明?

文章目录 引子使用技巧技巧1&#xff1a;使用分隔符技巧2&#xff1a;结构化输出技巧3&#xff1a;整理操作步骤技巧4&#xff1a;做示范技巧5&#xff1a;给定具体的步骤技巧6&#xff1a;生成摘要技巧7&#xff1a;情感分析 好问题的三要素总结 引子 你有没有发现&#xff0…

ENVI自动地理配准栅格图像(至少一一幅图像含有地理信息)

本文就介绍一种在ENVI 5.3 (64-bit) 软件中&#xff0c;自动生成地面控制点&#xff0c;从而对遥感影像进行地理配准的方法。 我们先来看一下本文需要实现的需求。现有以下两景遥感影像&#xff0c;其中一景含有地理参考信息&#xff0c;而另一景则不含有任何地理参考信息。在…

STL常用容器_1

目录 一、string容器 1、基本概念 2、构造函数 3、赋值操作 4、字符串拼接 5、查找和替换 6、字符串比较 7、字符存取 8、插入与删除 9、获取字串 二、vector容器 1、基本概念 2、构造函数 3、赋值操作 4、容量和大小 5、插入和删除 6、数据存取 7、互换容器…

Apache Hive

Hive的概念 Hive是Facebook开源出来&#xff0c;后来贡献给力Apache .宗旨是&#xff1a;提高分析数据的能力降低分析数据的开发成本。 Hive是基于 Hadoop 的一个数据仓库工具&#xff0c;用于分析数据的。 为什么说Hive是基于Hadoop的呢&#xff1f; ​ #作为一款数据仓库软件…