[Linux]:线程(三)

news2025/2/3 20:50:58

img

✨✨ 欢迎大家来到贝蒂大讲堂✨✨

🎈🎈养成好习惯,先赞后看哦~🎈🎈

所属专栏:Linux学习
贝蒂的主页:Betty’s blog

1. POSIX 信号量

1.1 信号量的概念

为了解决多执行流访问临界区,造成数据不一致等问题,我们除了使用互斥锁外,我们还可以使用一种 POSIX信号量的方法。

当我们运用互斥锁来保护临界资源时,意味着我们把这块临界资源视为一个不可分割的整体,在同一时刻只准许一个执行流对其进行访问。

其实我们也能将这块临界资源进一步划分成多个区域。当多个执行流有访问临界资源的需求时,若让这些执行流同时去访问临界资源的不同区域,此时也并不会引发数据不一致等问题。信号量就是基于此的解决方法。

POSIX信号量本质上是一个计数器,用于衡量临界资源中的资源数目。它对临界资源内部的资源数进行统计,同时操作系统为其提供了一种对临界资源的预定机制。所有执行流在访问临界资源之前,必须先申请信号量。

画板

信号量的 PV 操作:

  • P操作:我们将申请信号量称为P操作,申请信号量的本质就是申请获得临界资源中某块资源的使用权限,当申请成功时临界资源中资源的数目应该减一,因此P操作的本质就是让计数器减一。
  • V操作:我们将释放信号量称为V操作,释放信号量的本质就是归还临界资源中某块资源的使用权限,当释放成功时临界资源中资源的数目就应该加一,因此V操作的本质就是让计数器加一。

并且由于因信号量的 PV 操作同样属于临界资源,所以 PV 操作肯定是原子的。

值得注意的是: 虽然 POSIX信号量和SystemV信号量作用相同,都是用于同步操作,但POSIX信号量常用于线程间同步,而 SystemV 信号量常用于进程间通信。

1.2 信号量的接口

1.2.1 初始化信号量

我们首先需要使用 sem_init初始化信号量,其用法如下:

  1. 函数接口:int sem_init(sem_t *sem, int pshared, unsigned int value);
  2. 参数:
  • sem:需要初始化的信号量。
  • pshared:传入0值表示线程间共享,传入非零值表示进程间共享。
  • value:信号量的初始值(计数器的初始值)。
  1. 返回值:初始化信号量成功返回0,失败返回-1。
1.2.2 销毁信号量

在使用完信号量之后,我们就需要用 sem_destory 对其进行销毁,其用法如下:

  1. 函数接口:int sem_destroy(sem_t *sem);
  2. 参数:
  • sem:需要销毁的信号量。
  1. 返回值:销毁信号量成功返回0,失败返回-1。
1.2.3 申请信号量

申请信号量也就是 P 操作,我们需要使用 sem_wait函数,其用法如下:

  1. 函数接口:int sem_wait(sem_t *sem);
  2. 参数:
  • sem:需要申请的信号量。
  1. 返回值:申请信号量成功返回0,信号量的值减一。申请信号量失败返回-1,信号量的值保持不变。如果信号量为 0,则该执行流会被阻塞,直至信号量大于 0。
1.2.4 释放信号量

释放信号量也就是 V 操作,我们需要使用 sem_post函数,其用法如下:

  1. 函数接口:int sem_post(sem_t *sem);
  2. 参数:
  • sem:需要释放的信号量。
  1. 返回值:释放信号量成功返回0,信号量的值加一。释放信号量失败返回-1,信号量的值保持不变。

如果信号量的初始值为1,那么此时信号量所描述的临界资源只有一份,这个临界资源也只能同时被一个执行流访问。此时信号量的作用基本等价于互斥锁,这种信号量我们称为二元信号量。

比如我们下面可以通过二元信号量实现我们的抢票逻辑:

#include <iostream>
#include <cstdio>
#include <string>
#include <unistd.h>
#include <pthread.h>
#include <semaphore.h>
class Sem
{
public:
    Sem(int num)
    {
        sem_init(&_sem, 0, num);
    }
    ~Sem()
    {
        sem_destroy(&_sem);
    }
    void P()
    {
        sem_wait(&_sem);
    }
    void V()
    {
        sem_post(&_sem);
    }

private:
    sem_t _sem;
};
int tickets = 1000;
Sem sem(1);
void *getTickets(void *args)
{
    uint64_t i = (uint64_t)args;
    char buffer[64] = {0};
    snprintf(buffer, sizeof(buffer), "thread %llu", i);
    while (true)
    {
        sem.P();
        if (tickets > 0)
        {
            usleep(1000);
            std::cout << buffer << " get a ticket,tickets left: " << --tickets << std::endl;
            sem.V();
        }
        else
        {
            sem.V();
            break;
        }
    }
    std::cout << buffer << " quit ..." << std::endl;
    return nullptr;
}
int main()
{
    pthread_t tids[5];
    for (uint64_t i = 0; i < 5; i++)
    {
        pthread_create(tids + i, nullptr, getTickets, (void *)i);
    }
    for (int i = 0; i < 5; i++)
    {
        pthread_join(tids[i], nullptr);
    }
    return 0;
}

2. 生产者消费者模型

2.1 概念

生产者 - 消费者模型是一种经典的多线程或多进程同步模型。它主要用于解决在数据生产和数据消费速度不一致的情况下,如何安全、高效地处理数据的问题。

在这个模型中,有两类角色:生产者消费者。生产者负责生产数据,例如在一个文件读取系统中,生产者可能是读取文件内容并将其转换为特定格式数据的线程或进程;消费者则负责消费(处理)生产者生产的数据,比如将读取到的数据进行进一步的分析或者存储到数据库中的线程或进程。

画板

利用该模型我们能实现生产者与消费者之间的解耦,并且生产者在生产时,其它生产者可以获取数据,消费者可以处理数据,消费者在消费时也是同理,一定程度上实现了并发。

2.2 特点

生产者-消费者模型一般具有以下三个特点:

  • 三种关系: 生产者和生产者(互斥关系)、消费者和消费者(互斥关系)、生产者和消费者(互斥关系、同步关系)。
  • 两种角色: 生产者和消费者。(通常由进程或线程承担)
  • 一个交易场所: 通常指的是内存中的一段缓冲区。

因为容器是能够被多个执行流访问的一个共享资源,所以生产者与生产者,消费者与消费者,生产者与消费者之间是一个互斥关系,而我们访问数据一定是生产者先生产,消费者再消费,所以生产者与消费者之间是一个同步关系。

3. 生产者消费者模型的实现

3.1 基于阻塞队列实现

阻塞队列就是队列的一种,但其要求:

  • 当队列为空时,从队列获取元素的操作将会被阻塞,直到队列中放入了元素。
  • 当队列满时,往队列里存放元素的操作会被阻塞,直到有元素从队列中取出。

其中阻塞队列最典型的应用场景实际上就是管道的实现。

画板

首先我们可以先实现 BlockingQueue的框架:首先我们需要一个队列 _q 作为成员变量以及表示其容量的 _cap,并且因为涉及多执行流访问,需要一把互斥锁 _mutex,最后我们还需要两个条件变量 _empty与·_full分别表示当我们队列为空时,执行消费的执行流需加入 _empty 条件变量与当我们队列为满时,执行生产的执行流需加入该条件变量 _full

#include <iostream>
#include <pthread.h>
#include <queue>
#include <unistd.h>
const int defaultnum = 5;
template <class T>
class BlockQueue
{
    bool IsFull()
    {
        return _q.size() == _cap;
    }
    bool IsEmpty()
    {
        return _q.empty();
    }
public:
    BlockQueue(int cap = defaultnum)
        : _cap(cap)
    {
        pthread_mutex_init(&_mutex, nullptr);
        pthread_cond_init(&_full, nullptr);
        pthread_cond_init(&_empty, nullptr);
    }
    ~BlockQueue()
    {
        pthread_mutex_destroy(&_mutex);
        pthread_cond_destroy(&_full);
        pthread_cond_destroy(&_empty);
    }
    void Push(const T&data);
    void Pop(T&data);
private:
    std::queue<T> _q;
    int _cap;
    pthread_mutex_t _mutex;
    pthread_cond_t _full;
    pthread_cond_t _empty;
};

并且我们实现生产 Push与消费 Pop操作也十分简单,生产时如果队列为满,则加入条件变量 _full 等待,没有则正常生产,生产完毕后该队列一定有数据,这时我们就需要唤醒 _empty条件变量执行消费操作。而消费操作正好对应,如果消费时如果队列为空,则加入条件变量 _empty 等待,否则正常消费,消费完毕后该队列一定不为空,这时我们就需要唤醒 _full条件变量执行生产操作。并且生产与消费操作都属于临界资源,所以需要加锁。

void Push(const T&data)
{
    pthread_mutex_lock(&_mutex);
    while(IsFull())
    {
        pthread_cond_wait(&_full,&_mutex);
    }
    _q.push(data);
    pthread_mutex_unlock(&_mutex);
    pthread_cond_signal(&_empty);
}
void Pop(T&data)
{
    pthread_mutex_lock(&_mutex);
    while(IsEmpty())
    {
        pthread_cond_wait(&_empty,&_mutex);
    }
    data=_q.front();
    _q.pop();
    pthread_mutex_unlock(&_mutex);
    pthread_cond_signal(&_full);
}

需要注意的是,<font style="color:rgb(28, 31, 35);">pthread_cond_wait</font> 函数作为让当前执行流进行等待的函数,存在调用失败的可能性,若调用失败,该执行流会继续往后执行。

在多生产者的情形下,当消费者消费了一个数据后,若使用 <font style="color:rgb(28, 31, 35);">pthread_cond_broadcast</font> 函数唤醒多个生产者,此时若阻塞队列仅有一个空位,且唤醒的生产者与消费者竞争,当生产者持续竞争锁成功时,就可能出现错误。鉴于此,为避免上述情况发生,必须让线程被唤醒后再次进行判断,以确认是否真正满足生产消费条件,所以这里要用 <font style="color:rgb(28, 31, 35);">while</font> 进行判断。

最后我们创建多个线程,进行对应的生产与消费操作即可。

#include "BlockQueue.hpp"
#include <cstdlib>
#include <ctime>
void *Producer(void *args)
{
    pthread_detach(pthread_self());
    BlockQueue<int> *bq = static_cast<BlockQueue<int> *>(args);
    while (true)
    {
        int data = rand() % 100 + 1;
        bq->Push(data); 
        std::cout << "Producer: " << data << std::endl;
    }
}
void *Consumer(void *args)
{
    pthread_detach(pthread_self());
    BlockQueue<int> *bq = static_cast<BlockQueue<int> *>(args);
    while (true)
    {
        int data = 0;
        bq->Pop(data); 
        std::cout << "Consumer: " << data << std::endl;
        sleep(1);
    }
}
int main()
{
    srand((unsigned int)time(nullptr));
    BlockQueue<int> *bq = new BlockQueue<int>;
    for (int i = 0; i < 5; i++)
    {
        pthread_t tid;
        pthread_create(&tid, nullptr, Producer, bq);
    }
    for (int i = 0; i < 5; i++)
    {
        pthread_t tid;
        pthread_create(&tid, nullptr, Consumer, bq);
    }
    while(true);
    return 0;
}

3.2 基于循环队列实现

我们同样也可以通过循环队列来实现生产者消费者模型,并且在循环队列不为空或者满的情况下,生产者与消费者可以同步执行。并且要求:

  • 当生产和消费指向同一个资源的时候,只能一个执行流访问。为空的时候,由生产者去访问;为满的时候,由消费者去访问。
  • 消费者不能超过生产者。
  • 生产者不能把消费者套圈,因为这样会导致数据被覆盖。

画板

首先我们可以先实现 RingQueue的框架:首先我们可以使用数组来模仿队列 _q ,以及表示其容量的 _cap,然后用 _p_pos_c_pos分别表示生产者与消费者访问数据的下标,其中我们需要两个信号量 _blank_sem与·_data_sem分别表示队列未填数据的个数与已填数据的个数,并且因为涉及多执行流访问,我们最后要用两把互斥锁 _p_mutex_c_mutex来保护生产与消费的临界资源。

#pragma once
#include <iostream>
#include <pthread.h>
#include <vector>
#include <semaphore.h>
#include <unistd.h>
const int defaultnum = 5;
template <class T>
class RingQueue
{
    void P(sem_t &s)
    {
        sem_wait(&s);
    }
    void V(sem_t &s)
    {
        sem_post(&s);
    }
    void Lock(pthread_mutex_t *mutex)
    {
        pthread_mutex_lock(mutex);
    }
    void UnLock(pthread_mutex_t *mutex)
    {
        pthread_mutex_unlock(mutex);
    }

public:
    RingQueue(int cap = defaultnum)
        : _cap(cap), _p_pos(0), _c_pos(0)
    {
        _q.resize(_cap);
        sem_init(&_blank_sem, 0, _cap);
        sem_init(&_data_sem, 0, 0);
        pthread_mutex_init(&_p_mutex, nullptr);
        pthread_mutex_init(&_c_mutex, nullptr);
    }
    ~RingQueue()
    {
        sem_destroy(&_blank_sem);
        sem_destroy(&_data_sem);
        pthread_mutex_destroy(&_p_mutex);
        pthread_mutex_destroy(&_c_mutex);
    }
    void Push(const T &data);
    void Pop(T &data);
private:
    std::vector<T> _q;
    int _cap;
    int _p_pos;
    int _c_pos;
    sem_t _blank_sem;
    sem_t _data_sem;
    pthread_mutex_t _p_mutex;
    pthread_mutex_t _c_mutex;
};

我们实现生产 Push与消费 Pop操作也十分简单,生产时如果队列为满,那么未填数据个数 _blank_sem为 0,该执行流就会被阻塞,没有则正常生产。而消费操作正好对应,如果消费时如果队列为空,那么已填数据个数 _data_sem为 0,该执行流就会被阻塞,否则就正常消费,并且生产与消费操作都属于临界资源,所以需要加锁。

void Push(const T &data)
{
    P(_blank_sem);
    Lock(&_p_mutex);
    _q[_p_pos] = data;
    _p_pos++;
    _p_pos %= _cap;
    UnLock(&_p_mutex);
    V(_data_sem);
}
void Pop(T &data)
{
    P(_data_sem);
    Lock(&_c_mutex);
    data = _q[_c_pos];
    _c_pos++;
    _c_pos %= _cap;
    UnLock(&_c_mutex);
    V(_blank_sem);
}

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

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

相关文章

Java中的break、continue和return语句

break、continue和return break语句引入基本介绍基本语法示意图注意事项练习String字符串的比较 continue跳转控制语句基本介绍基本语法示意图 return跳转控制语句 break语句 引入 随机生成1-100的一个数&#xff0c;直到生成了97这个数&#xff0c;看看你一共用了几次&#…

前端性能优化 面试如何完美回答

前言 性能优化是目前在面试中被问到非常多的问题&#xff0c;主要就是通过各种算和技术来提高页和应用的速度和用户体前端性能优化的问题并不好回答 在回答的时候干万不要掉进一个误区&#xff0c;认为性能优化只是几个技术点而已&#xff0c;事实上性能优化涉及到的是多方面的…

【2024年10月测试通过】conda下使用虚拟环境安装最新版pytorch2.4+cuda12.4

开头先说重点&#xff1a; 1.采用conda的虚拟环境&#xff0c;会在沙盒环境下安装好所有所需包&#xff0c;而且该虚拟环境拷贝给其他人员可以直接用&#xff0c;很方便。 2.pytorch官网访问不了&#xff0c;有一个国内镜像推荐&#xff0c;地址为PyTorch - PyTorch 中文 3.…

unity ps 2d animation 蛇的制作

一、PS的使用 1.打开PS 利用钢笔工具从下往上勾勒填充 2.复制图层&#xff0c;Ctrl T,w调为-100% 3.对齐图层并继续用钢笔工具进行三角勾勒 3.画眼睛,按U快捷键打开椭圆工具&#xff0c;按住Shift可以画圆&#xff0c;填充并复制图层对称。 4.画笔工具&#xff0c;打开小…

Golang | Leetcode Golang题解之第458题可怜的小猪

题目&#xff1a; 题解&#xff1a; func poorPigs(buckets, minutesToDie, minutesToTest int) int {if buckets 1 {return 0}combinations : make([][]int, buckets1)for i : range combinations {combinations[i] make([]int, buckets1)}combinations[0][0] 1iterations…

「漏洞复现」用友U8 CRM config/fillbacksettingedit.php SQL注入漏洞

0x01 免责声明 请勿利用文章内的相关技术从事非法测试&#xff0c;由于传播、利用此文所提供的信息而造成的任何直接或者间接的后果及损失&#xff0c;均由使用者本人负责&#xff0c;作者不为此承担任何责任。工具来自网络&#xff0c;安全性自测&#xff0c;如有侵权请联系删…

蓝牙定位的MATLAB仿真程序(基于信号强度,平面内的定位,四个蓝牙基站)

这段代码通过RSSI信号强度实现了蓝牙定位,展示了如何使用锚点位置和测量的信号强度来估计未知点的位置。它涵盖了信号衰减模型、距离计算和最小二乘法估计等基本概念。通过图形化输出,用户可以直观地看到真实位置与估计位置的关系。 文章目录 蓝牙定位原理蓝牙定位的原理优缺…

【综合性渗透利器】- TscanPlus

如果你在寻找一款轻量级、实用且开源的漏洞扫描工具&#xff0c;那么 TscanPlus 绝对值得一试。这款工具由 TideSec 团队打造&#xff0c;以其简洁、高效、易用的特点&#xff0c;广受好评&#xff0c;目前在github上拥有1.5k star。 为什么推荐 TscanPlus&#xff1f; 无论你…

基于Zynq SDIO WiFi移植一(支持2.4/5G)

基于SDIO接口的WIFI&#xff0c;在应用上&#xff0c;功耗低于USB接口&#xff0c;且无须USB Device支持&#xff0c;满足某些应用场景 1 硬件连接 2 Vivado工程配置 3 驱动编译 3.1 KERNRL CONFIG (build ENV) 修改 export KERNELPATH<path of kernel header>export T…

【web安全】——SSRF服务器端请求伪造

1.SSRF漏洞基础 1.1SSRF漏洞概述与成因 SSRF(Server-Side Request Forgery:服务器端请求伪造) 是一种由攻击者构造形成由服务端发起请求的一个安全漏洞。 一般情况下&#xff0c;SSRF攻击的目标是从外网无法访问的内部系统。&#xff08;正是因为它是由服务端发起的&#xf…

Java--IO高级流

缓冲流 缓冲流,也叫高效流&#xff0c;是对4个基本的FileXxx 流的增强&#xff0c;所以也是4个流&#xff0c;按照数据类型分类&#xff1a; 字节缓冲流&#xff1a;BufferedInputStream&#xff0c;BufferedOutputStream 字符缓冲流&#xff1a;BufferedReader&#xff0c;Buf…

Python | Leetcode Python题解之第458题可怜的小猪

题目&#xff1a; 题解&#xff1a; class Solution:def poorPigs(self, buckets: int, minutesToDie: int, minutesToTest: int) -> int:if buckets 1:return 0combinations [[0] * (buckets 1) for _ in range(buckets 1)]combinations[0][0] 1iterations minutesT…

StreamProvider组件的用法

文章目录 1 概念介绍2 使用方法3 示例代码我们在上一章回中介绍了通道相关的内容,本章回中将介绍StreamProvider组件.闲话休提,让我们一起Talk Flutter吧。 1 概念介绍 在Flutter中Stream是经常使用的组件,对该组件的监听可void main() {///让状态栏和程序的appBar融为一体…

yub‘s Algorithm Adventure Day6

链表相交 link&#xff1a;面试题 02.07. 链表相交 - 力扣&#xff08;LeetCode&#xff09; 思路分析 看到描述很直接的想到双指针&#xff0c;但是看到题解之后被K佬的神级理解折服&#xff0c;太妙了&#xff01; 双指针 public class Solution {public ListNode getIn…

计算机毕业设计 乡村生活垃圾管理系统的设计与实现 Java实战项目 附源码+文档+视频讲解

博主介绍&#xff1a;✌从事软件开发10年之余&#xff0c;专注于Java技术领域、Python人工智能及数据挖掘、小程序项目开发和Android项目开发等。CSDN、掘金、华为云、InfoQ、阿里云等平台优质作者✌ &#x1f345;文末获取源码联系&#x1f345; &#x1f447;&#x1f3fb; 精…

设计模式、系统设计 record part03

创建者模式 1.创建、使用&#xff0c;二者分离 2.降低&#xff0c;耦合度 3.使用者&#xff0c;不用关注&#xff0c;对象的创建细节 工厂模式&#xff1a; 1.对象由工厂生产&#xff0c; 2.使用者与工厂交流&#xff0c;不与对象直接打交道&#xff0c; 3.在工厂里直接更换对象…

使用 Vertex AI Gemini 模型和 Elasticsearch Playground 快速创建 RAG 应用程序

作者&#xff1a;来自 Elastic Jeff Vestal 在这篇博客中&#xff0c;我们将使用 Elastic 的 Playground 和 Vertex AI API 将 Elasticsearch 连接到 Google 的 Gemini 1.5 聊天模型。将 Gemini 模型添加到 Playground 使 Google Cloud 开发人员能够快速建立 LLM、测试检索、调…

开源的云平台有哪些?

开源云平台为用户提供了构建、管理和运行云基础设施及应用的能力&#xff0c;同时允许社区参与开发和改进。以下是一些知名的开源云平台&#xff1a; 1. OpenStack 简介&#xff1a;OpenStack&#xff1a;一个广泛使用的开源云平台&#xff0c;它由多个组件组成&#xff0c;提…

PriorityQueue分析

概述 PriorityQueue&#xff0c;优先级队列&#xff0c;一种特殊的队列&#xff0c;作用是能保证每次取出的元素都是队列中权值最小的&#xff08;Java的优先队列每次取最小元素&#xff0c;C的优先队列每次取最大元素&#xff09;。元素大小的评判可以通过元素本身的自然顺序…

linux信号 | 学习信号四步走 | 透析信号是如何被处理的?

前言&#xff1a;本节内容讲述linux信号的捕捉。 我们通过前面的学习&#xff0c; 已经学习了信号的概念&#xff0c; 信号的产生&#xff0c; 信号的保存。 只剩下信号的处理。 而信号的处理我们应该着重注意的是第三种处理方式——信号的捕捉。 也就是说&#xff0c; 这篇文章…