【Linux】线程同步和互斥

news2024/11/17 3:53:36

目录

  • 一、线程互斥
    • 1.相关概念
    • 2.互斥锁(mutex)
    • 3.互斥锁的原理
    • 4.自定义封装一个锁
  • 二、可重入和线程安全
  • 三、死锁
    • 死锁概念
    • 死锁四个必要条件
    • 如何避免死锁
  • 四、线程同步
    • 1.条件变量
      • 概念
      • 条件变量接口
      • 基于阻塞队列实现生产者消费者模型
    • 2.信号量
      • 概念
      • 信号量操作接口
      • 基于环形队列的生产者消费者模型
  • 五、总结

一、线程互斥

1.相关概念

1.临界资源:多线程执行流共享的资源,且一次只能允许一个执行流访问的资源就叫做临界资源。(多线程、多进程打印数据)

2.临界区:每个线程内部,访问临界资源的代码,就叫做临界区。

3.互斥:任何时刻,互斥保证有且只有一个执行流进入临界区,访问临界资源,通常对临界资源起保护作用

4.原子性:不会被任何调度机制打断的操作,该操作只有两态,要么完成,要么不执行 。

实现一个小实例

#include<iostream>
#include<unistd.h>
#include<stdio.h>

using namespace std; int ticket=10000;

void* threadRoutinue(void* args) {
    const char* name=static_cast<const char*>(args);
    while(true)
    {
        if(ticket>0)
        {
            usleep(1000);//模拟抢票花费时间
            cout<<name<<"get a ticket: "<<ticket--<<endl;
        }
        else{
            break;
        }
    }
    return nullptr; }

int main() {
    //创建线程模拟抢票
    pthread_t tid[4];
    int n=sizeof(tid)/sizeof(tid[0]);
    for(int i=0;i<n;i++)
    {
        char buffer[64];
        snprintf(buffer,sizeof(buffer),"thread_%d",i);
        pthread_create(tid+i,nullptr,threadRoutinue,buffer);
    }

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

    return 0; } 

在这里插入图片描述

从程序中可以看到,票数到0的时候就没有票了,线程就应该退出了。

但是结果中,票数甚至被抢到了负数,这是怎么回事。

这里提一个问题,这里对票(临界资源)的访问是原子的吗?(是安全的吗?) 答案肯定不是!!
在这里插入图片描述
可能在一个线程A中,刚刚将tickets加载到内存上,线程A就被切走了,这时线程A的数据和上下文被保存,线程A从CPU上被剥离。

线程B开始抢票,如果他的竞争力非常强,一次运行后抢到了1000张票。

线程B执行完后线程A又来了,他会从上次执行的地方继续执行,但是他上次保存的tickets的数据是10000,所以抢到了一张票后,将剩余的9999张票写回内存,本来线程B执行完后还剩9000张票,但是线程A执行完后剩余的票数反而增多了。

2.互斥锁(mutex)

对于上面的抢票程序,要想使每个线程正确的抢票就要保证:当一个线程在进入到抢票环节时,其他线程不能进行抢票。
所以就可以对抢票环节加互斥锁。

pthread_mutex_init、pthread_mutex_destroy:对线程锁进行初始化和销毁

#include <pthread.h> 
// pthread_mutex_t mutex: 锁变量,所有线程都可看到 
int pthread_mutex_destroy(pthread_mutex_t *mutex);// 销毁锁 
int pthread_mutex_init(pthread_mutex_t *restrict mutex,constpthread_mutexattr_t *restrict attr);// 初始化锁 
// attr: 锁属性,我们传入空指针就可  
// 如果将锁定义为静态或者全局的,可以使用宏直接初始化,且不用销毁 
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;

pthread_mutex_lock、int pthread_mutex_unlock:对线程进行加锁和解锁

#include <pthread.h> 
int pthread_mutex_lock(pthread_mutex_t *mutex);   
int pthread_mutex_unlock(pthread_mutex_t *mutex); 

对抢票小demo进行加锁

#include<iostream>
#include<unistd.h>
#include<stdio.h>

using namespace std; int ticket=10000;  //临界资源
pthread_mutex_t mutex;

void* threadRoutinue(void* args) {
    const char* name=static_cast<const char*>(args);
    while(true)
    {
        pthread_mutex_lock(&mutex);
        if(ticket>0)
        {
            usleep(1000);//模拟抢票花费时间
            cout<<name<<" get a ticket: "<<ticket--<<endl;
            pthread_mutex_unlock(&mutex);
        }
        else{
            cout<<name<<"票抢完了"<<endl;
            pthread_mutex_unlock(&mutex);
            break;
        }
        usleep(1000);
    }
    return nullptr; }

int main() {
    pthread_mutex_init(&mutex,nullptr);
    //创建线程模拟抢票
    pthread_t tid[4];
    int n=sizeof(tid)/sizeof(tid[0]);
    for(int i=0;i<n;i++)
    {
        char buffer[64];
        snprintf(buffer,sizeof(buffer),"thread_%d",i);
        pthread_create(tid+i,nullptr,threadRoutinue,buffer);
    }

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

    pthread_mutex_destroy(&mutex);    
    return 0; } 

多线程临界资源原子
在这里插入图片描述

细节:
1.凡是访问同一个临界资源的线程,都要进行加锁保护,而且必须加同一把锁,这是一个规则,不能有例外
2.每一个线程访问临界资源之前,得加锁,加锁本质是给 临界区加锁,加锁的粒度尽量细一些。
3.线程访问临界区的时候,需要先加锁 -> 所以线程都必须看到同一把锁 -> 锁本身就是公共资源 -> 锁如何保证自己的安全? -> 加锁和解锁本身就是原子的。
4.临界区可以是一行代码,可以是一批代码,a.线程可能被切换? 当然可能 b.切换会有影响嘛? 没有,因为一个线程申请一个锁以后,该线程被临时切换,其他任何线程没有办法进入临界区,无法申请到锁,所以无法访问到临界资源。
5.这也正是体现互斥带来的串行化的表现,站在其他线程的角度,对其他线程有意义的状态是:锁被申请(持有锁),锁被释放(不持有锁),原子性。

3.互斥锁的原理

以抢票程序为例,当线程需要访问临界资源时,需要先访问mtx,为了所有的线程都能看到它,所以锁肯定是全局的。

且锁本身也是临界资源。那么如何保证锁本身是安全的,即获取锁的过程是安全的。
其原理是:加锁(lock)、解锁(unlock)的过程是原子的!

那怎样才算是原子的呢:一行代码被翻译成汇编后只有一条汇编,就是原子的。

为了实现互斥锁操作,大多数体系结构都提供了swapexchange指令。

该指令的作用是把寄存器和内存单元的数据相交换,由于只有一条指令,保证了原子性。

即使是多处理器平台,访问内存的 总线周期也有先后,一个处理器上的交换指令执行时另一个处理器的交换指令只能等待总线周期。

在这里插入图片描述

当线程申请到锁之后,进入到临界区访问临界资源,这时线程也可能被切走,被切走后会保护上下文,而锁数据也在上下文中。

所以锁也被带走了,所以即便是该线程被挂起了,其他线程也不能申请到锁,也不能进入临界区。

必须等待拥有锁的线程释放锁之后才能申请到锁。

4.自定义封装一个锁

#pragma once
#include<iostream>
#include<pthread.h>

//封装锁

class _Mutex
{
public:
    _Mutex(pthread_mutex_t* mutex):_mutex(mutex)
    {}

    void lock()
    {
        pthread_mutex_lock(_mutex);
    }

    void unlock()
    {
        pthread_mutex_unlock(_mutex);
    }
private:
    pthread_mutex_t* _mutex;
};


class lockGuard
{
public:
    lockGuard(pthread_mutex_t* mutex):_mutex(mutex)
    {
        _mutex.lock();
    }

    ~lockGuard()
    {
        _mutex.unlock();
    }

private:
    _Mutex _mutex;
};

我们可以使用我们自己封装的锁解决抢票问题
在这里插入图片描述

二、可重入和线程安全

线程安全: 线程安全指的是在多线程编程中,多个线程对临界资源进行争抢访问而不会造成数据二义或程序逻辑混乱的情况。常见对全局变量或者静态变量进行操作,并且没有锁保护的情况下,会出现该问题。

重入: 同一个函数被不同的执行流调用,当前一个流程还没有执行完,就有其他的执行流再次进入,我们称之为重入。一个函数在重入的情况下,运行结果不会出现任何不同或者任何问题,则该函数被称为可重入函数,否则,是不可重入函数

线程安全的实现,通过同步与互斥实现

具体互斥的实现可以通过互斥锁和信号量实现、而同步可以通过条件变量与信号量实现。

常见的线程不安全的情况:

  • 不保护共享变量的函数

  • 函数状态随着被调用,状态发生变化的函数

  • 返回指向静态变量指针的函数

  • 调用线程不安全函数的函数

常见不可重入的情况:

  • 调用了malloc/free函数,因为malloc函数是用全局链表来管理堆的

  • 调用了标准I/O库函数,标准I/O库的很多实现都以不可重入的方式使用全局数据结构

  • 可重入函数体内使用了静态的数据结构

可重入与线程安全联系:

  • 函数是可重入的,那就是线程安全的

  • 函数是不可重入的,那就不能由多个线程使用,有可能引发线程安全问题

  • 如果一个函数中有全局变量,那么这个函数既不是线程安全也不是可重入的。

可重入与线程安全区别:

  • 可重入函数是线程安全函数的一种

  • 线程安全不一定是可重入的,而可重入函数则一定是线程安全的。

  • 如果将对临界资源的访问加上锁,则这个函数是线程安全的,但如果这个重入函数锁还未释放则会产生死锁

三、死锁

死锁概念

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

死锁四个必要条件

  • 互斥条件:一个资源每次只能被一个执行流使用

  • 请求与保持条件:一个执行流因请求资源而阻塞时,对已获得的资源保持不放

  • 不剥夺条件:一个执行流已获得的资源,在末使用完之前,不能强行剥夺

  • 循环等待条件:若干执行流之间形成一种头尾相接的循环等待资源的关系

如何避免死锁

核心思想:破坏死锁的4个必要条件中任意一个!

  • 不加锁

  • 主动释放锁

  • 按顺序申请锁

  • 资源一次性分配

破坏死锁的一个小demo(主动释放锁)

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

using namespace std;

pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;

void *pthreadRoutinue(void *args) {  
    pthread_mutex_lock(&mutex); //加锁
    cout<<"I get a mutex"<<endl;

    pthread_mutex_lock(&mutex); //产生死锁
    cout<<"i alive again"<<endl;

    return nullptr; }

int main() {
    pthread_t pid;
    pthread_create(&pid, nullptr, pthreadRoutinue, nullptr);

    sleep(3);
    cout<<"main thread run"<<endl;

    pthread_mutex_unlock(&mutex);//主线程区解锁
    cout<<"main thread unlock"<<endl;

    sleep(3);
    return 0; } 

在这里插入图片描述

四、线程同步

同步:在保证数据安全的前提下,让线程能够按照某种特定的顺序访问临界资源,从而有效避免饥饿问 题,叫做同步 。

1.条件变量

概念

与互斥锁不同,条件变量是用来等待而不是用来上锁的。条件变量用来自动阻塞一个线程,直到某特殊情况发生为止。通常条件变量和互斥锁同时使用。

条件变量使我们可以睡眠等待某种条件出现。条件变量是利用线程间共享的全局变量进行同步的一种机制,主要包括两个动作:一个线程等待"条件变量的条件成立"而挂起;另一个线程使"条件成立"(给出条件成立信号)。

条件变量接口

pthread_cond_init、pthread_cond_destroy:初始化、销毁条件变量

#include <pthread.h> 
int pthread_cond_destroy(pthread_cond_t *cond);
// pthread_cond_t:条件变量类型,类似pthread_mutex_t int
pthread_cond_init(pthread_cond_t *restrict cond,constpthread_condattr_t *restrict attr);   

// 如果是静态或全局的条件变量可使用宏初始化:
pthread_cond_t cond = PTHREAD_COND_INITIALIZER; 

pthread_cond_wait、pthread_cond_signal:等待条件、唤醒线程

#include <pthread.h>   

// 等待条件满足 
int pthread_cond_wait(pthread_cond_t *restrict cond,pthread_mutex_t *restrict mutex);   
// 唤醒一个线程,在cond等待队列里的第一个线程 
int pthread_cond_signal(pthread_cond_t *cond);
// 一次唤醒所有线程 
int pthread_cond_broadcast(pthread_cond_t *cond); ```

demo

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

using namespace std;
#define num 5

int ticket =1000; pthread_mutex_t mutex=PTHREAD_MUTEX_INITIALIZER;
pthread_cond_t cond=PTHREAD_COND_INITIALIZER;

void *active(void *args) {  
    string name=static_cast<const char*>(args);
    while(true)
    {
        pthread_mutex_lock(&mutex);
        pthread_cond_wait(&cond,&mutex); //调用该函数,会自己释放锁
        cout<<name<<" 活动"<<endl;
        pthread_mutex_unlock(&mutex);
    }
    return nullptr; }

int main() {
    pthread_t tids[num];
    for(int i=0;i<num;i++)
    {
        char * name=new char[64];
        snprintf(name,64,"thread-%d",i); //线程创
        pthread_create(tids+i,nullptr,active,name);
    }

    sleep(3);
    while(true)
    {
        cout<<"main thread wakeup thread..."<<endl;
        //pthread_cond_signal(&cond); //唤醒cond队列中的一个线程
        pthread_cond_broadcast(&cond); //将cond队列中所以线程唤醒
        sleep(1);
    }
    for(int i=0;i<num;i++)
    {
        pthread_join(tids[i],nullptr); //线程等待
    }

    sleep(3);
    return 0; } 

在这里插入图片描述

基于阻塞队列实现生产者消费者模型

在这里插入图片描述

生产者消费者模式就是通过一个容器来解决生产者和消费者的强耦合问题

生产者和消费者彼此之间不直接通讯,而通过阻塞队列来进行通讯,所以生产者生产完数据之后不用等待消费者处理,直接扔给阻塞队列,消费者不找生产者要数据,而是直接从阻塞队列里取,阻塞队列就相当于一个缓冲区,平衡了生产者和消费者的处理能力。这个阻塞队列就是用来给生产者和消费者解耦的。

生产者消费者模型的优点:解耦 支持并发 支持忙闲不均

实则之前所讲的进程间通信中的管道通信就是一种生产者消费者模型,管道就是让不同的进程能够看到同一份资源,且管道自带同步和互斥的机制。进程间通信的本质其实就是生产者消费者模型。

代码:

blockQueue.hpp

#pragma once
#include <iostream>
#include <queue>
#include <pthread.h>

const int gcap = 5;

template <class T> class BlockQueue { public:
    BlockQueue(const int cap = gcap) : _cap(cap)
    {
        pthread_mutex_init(&_mutex, nullptr);
        pthread_cond_init(&_consumerCond, nullptr);
        pthread_cond_init(&_productorCond, nullptr);
    }

    ~BlockQueue()
    {
        pthread_mutex_destroy(&_mutex);
        pthread_cond_destroy(&_consumerCond);
        pthread_cond_destroy(&_productorCond);
    }

    bool isFull()
    {
        return _cap == _q.size();
    }

    void push(const T &in)
    { // 生产
        pthread_mutex_lock(&_mutex);
        while (isFull())  //细节1:使用while ,防止多线程被唤醒生产过多
        {   // 我们只能在临界区内部,判断临界资源是否就绪 注定了我们在当前一定是持有锁的
            pthread_cond_wait(&_productorCond, &_mutex); // 如果队列为满,生产者线程休眠 ,此时持有锁,wait会将锁unlock
            // 当线程醒来的时候,注定了继续从临界区内部继续运行,因为是在临界区被切走的
            // 注定了当线程被唤醒的时候,继续在pthread_cond_wait()函数继续向后运行,又要重新申请锁,申请成功才会彻底返回
        }
        // 没有满,让他继续生产
        _q.push(in);
        //策略,唤醒消费者线程
        pthread_cond_signal(&_consumerCond);
        pthread_mutex_unlock(&_mutex);
    }
    void pop(T *out)
    {
        pthread_mutex_lock(&_mutex);
        while (_q.empty())  //队列为空
        {
            pthread_cond_wait(&_consumerCond, &_mutex); 
        }

        *out = _q.front();
        _q.pop();
        //策略,唤醒生产者
        pthread_cond_signal(&_productorCond);
        pthread_mutex_unlock(&_mutex);
    }

private:
    std::queue<T> _q;
    int _cap;
    pthread_mutex_t _mutex;
    pthread_cond_t _consumerCond;  // 消费者对应的条件变量 空 wait
    pthread_cond_t _productorCond; // 生产者对应的条件变量 满 wait }; ```

task.hpp

#pragma once

#include <iostream>
#include <string>

class Task { public:
    Task() {}
    Task(int x, int y, char op) : _x(x), _y(y), _op(op), _result(0), _exitCode(0)
    {
    }

    void operator()()
    {
        switch (_op)
        {
        case '+':
            _result = _x + _y;
            break;
        case '-':
            _result = _x - _y;
            break;
        case '*':
            _result = _x * _y;
            break;
        case '/':
            if(_y==0) _exitCode=-1;
            else _result = _x / _y;
            break; 
        case '%':
            if(_y==0) _exitCode=-1;
            else _result = _x % _y;
            break;
        default:
            break;
        }
    }

    std::string formatArg()
    {
        return std::to_string(_x)+' '+ _op+ ' '+std::to_string(_y)+" = ";
    }
    std::string formatRes()
    {
        return std::to_string(_result) + "(" +std::to_string(_exitCode)+")";
    }
    ~Task(){}

private:
    int _x;
    int _y;
    char _op;

    int _result;
    int _exitCode; }; ```

main.cc

#include "blockQueue.hpp"
#include"task.hpp"
#include<ctime>
#include<unistd.h>

void *consumer(void *args) {
    BlockQueue<Task> *bq = static_cast<BlockQueue<Task> *>(args);
    while(true)
    {
        sleep(1);
        Task t;
        //1.将数据从blockqueue中获取  -- 获取到数据
        bq->pop(&t);
        t();
        //2.结合某种业务逻辑,处理数据!
        std::cout<<"consumer data: "<<t.formatArg()<<t.formatRes()<<std::endl;
    } }

void *productor(void *args) {
    srand((uint64_t)time(nullptr)^getpid());
    BlockQueue<Task> *bq = static_cast<BlockQueue<Task> *>(args);
    std::string opers="+-*/%";
    while(true)
    {
        //1.先通过某种渠道获取数据
        int x=rand()%20+1;
        int y=rand()%10+1;
        //2.将数据推送到blockqueue  -- 完成生产过程
        char op=opers[rand()%opers.size()];
        Task t(x,y,op);
        bq->push(t);
        std::cout<<"productor Task: "<<t.formatArg()<<"?"<<std::endl;
    } }

int main() {
    //BlockQueue<int> *bq = new BlockQueue<int>();
    BlockQueue<Task> *bq = new BlockQueue<Task>();
    // 单生产,单消费  支持多生产,多消费,因为看到同一快资源,使用同一把锁
    pthread_t c, p;
    pthread_create(&c, nullptr, consumer, bq);
    pthread_create(&p, nullptr, productor, bq);

    pthread_join(c, nullptr);
    pthread_join(p, nullptr);

    delete bq;
    return 0; } ```

运行结果:

在这里插入图片描述

2.信号量

概念

信号量本质就是一个计数器,用来描述临界区中临界资源的数目大小。

临界资源如果可以被划分为更小的资源,如果处理得当,我们也有可能让多个线程同时访问临界资源,从而实现并发。

但是每个线程想访问临界资源,都得先申请信号量资源。

信号量操作接口

申请信号量成功时,临界资源的数目会减一;释放信号量时,临界资源的数目会加一。

由于信号量是用来维护临界资源的,首先必须得保证自身是安全的,所以常规的对全局变量的++或–操作肯定是不行的。

P操作(申请信号量)
V操作(释放信号量)

sem_init、sem_destroy:初始化销毁信号量(具体用法与mutex和cond十分类似)

#include <semaphore.h> 
int sem_init(sem_t *sem, int pshared, unsigned int value); 
// pshared: 默认为0, value:信号量的初始值(count) 
int sem_destroy(sem_t *sem);   
// sem_t :信号量类型 // Link with -pthread. ```

sem_wait、sem_signal: 申请、释放信号量

int sem_wait(sem_t *sem); // P操作  
int
sem_post(sem_t *sem); // V操作    
// Link with -pthread. ```

基于环形队列的生产者消费者模型

在这里插入图片描述
但是现在的环形队列的判空判满不再使用中的两种方式判断,因为有了信号量可以判定。

队列为空的时候,消费者和生产者指向同一个位置。(生产和消费线程不能同时进行)(生产者执行)

队列为满的时候,消费者和生产者也指向同一个位置。(生产和消费线程不能同时进行)(消费者执行)

当队列不为空不为满的时候,消费者和生产者不指向同一个位置。(生产和消费线程可以并发执行)

根据上面三种情况,基于环形队列的生产者消费者模型应该遵守以下规则:

  • 生产者不能把消费者套一个圈

  • 消费者不能超过生产者

  • 当指向同一个位置的时候,要根据空、满状态,判断让谁先执行

  • 其他情况,消费者和生产者可以并发执行

实现:

ringQueue.hpp

#pragma once

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

static const int N = 5;

template <class T> class RingQueue { private:
    void P(sem_t &s)  
    {
        sem_wait(&s);
    }
    void V(sem_t &s)
    {
        sem_post(&s);
    }
    void Lock(pthread_mutex_t &m)
    {
        pthread_mutex_lock(&m);
    }
    void Unlock(pthread_mutex_t &m)
    {
        pthread_mutex_unlock(&m);
    }

public:
    RingQueue(int num = N) : _ring(num), _cap(num)
    {
        sem_init(&_data_sem, 0, 0);
        sem_init(&_space_sem, 0, num);
        _c_step = _p_step = 0;

        pthread_mutex_init(&_c_mutex, nullptr);
        pthread_mutex_init(&_p_mutex, nullptr);
    }
    // 生产
    void push(const T &in)
    {
        // 1. 可以不用在临界区内部做判断,就可以知道临界资源的使用情况
        // 2. 什么时候用锁,对应的临界资源,是否被整体使用
        P(_space_sem);  // P() 
        Lock(_p_mutex); 
        _ring[_p_step++] = in;
        _p_step %= _cap;
        Unlock(_p_mutex);
        V(_data_sem);
    }
    // 消费
    void pop(T *out)
    {
        P(_data_sem);
        Lock(_c_mutex);
        *out = _ring[_c_step++];
        _c_step %= _cap;
        Unlock(_c_mutex);
        V(_space_sem);
    }
    ~RingQueue()
    {
        sem_destroy(&_data_sem);
        sem_destroy(&_space_sem);

        pthread_mutex_destroy(&_c_mutex);
        pthread_mutex_destroy(&_p_mutex);
    }

private:
    std::vector<T> _ring;
    int _cap;         // 环形队列容器大小
    sem_t _data_sem;  // 只有消费者关心
    sem_t _space_sem; // 只有生产者关心
    int _c_step;      // 消费位置
    int _p_step;      // 生产位置

    pthread_mutex_t _c_mutex;
    pthread_mutex_t _p_mutex; };


task.hpp

#pragma once

#include <iostream>
#include <string>

class Task { public:
    Task() {}
    Task(int x, int y, char op) : _x(x), _y(y), _op(op), _result(0), _exitCode(0)
    {
    }

    void operator()()
    {
        switch (_op)
        {
        case '+':
            _result = _x + _y;
            break;
        case '-':
            _result = _x - _y;
            break;
        case '*':
            _result = _x * _y;
            break;
        case '/':
            if(_y==0) _exitCode=-1;
            else _result = _x / _y;
            break; 
        case '%':
            if(_y==0) _exitCode=-1;
            else _result = _x % _y;
            break;
        default:
            break;
        }
    }

    std::string formatArg()
    {
        return std::to_string(_x)+' '+ _op+ ' '+std::to_string(_y)+" = ";
    }
    std::string formatRes()
    {
        return std::to_string(_result) + "(" +std::to_string(_exitCode)+")";
    }
    ~Task(){}

private:
    int _x;
    int _y;
    char _op;

    int _result;
    int _exitCode; }; ```

main.cc

#include "ringQueue.hpp"
#include "task.hpp"
#include <ctime>
#include <pthread.h>
#include <memory>
#include <sys/types.h>
#include <unistd.h>
#include <cstring>

using namespace std;

const char *ops = "+-*/%";

void *consumerRoutine(void *args) {
    RingQueue<Task> *rq = static_cast<RingQueue<Task> *>(args);
    while (true)
    {
        Task t;
        rq->pop(&t);
        t();
        cout << "consumer done, 处理完成的任务是: " << t.formatRes() << endl;
    } }

void *productorRoutine(void *args) {
    RingQueue<Task> *rq = static_cast<RingQueue<Task> *>(args);
    while (true)
    {
        // sleep(1);
        int x = rand() % 100;
        int y = rand() % 100;
        char op = ops[(x + y) % strlen(ops)];
        Task t(x, y, op);
        rq->push(t);
        cout << "productor done, 生产的任务是: " << t.formatArg() << endl;
    } }

int main() {
    srand(time(nullptr) ^ getpid());
    RingQueue<Task> *rq = new RingQueue<Task>();
    // 单生产单消费
    // pthread_t c, p;
    // pthread_create(&c, nullptr, consumerRoutine, rq);
    // pthread_create(&p, nullptr, productorRoutine, rq);

    // pthread_join(c, nullptr);
    // pthread_join(p, nullptr);
    //多生产,多消费
    pthread_t c[3], p[2];
    for (int i = 0; i < 3; i++)
        pthread_create(c + i, nullptr, consumerRoutine, rq);
    for (int i = 0; i < 2; i++)
        pthread_create(p + i, nullptr, productorRoutine, rq);

    for (int i = 0; i < 3; i++)

        pthread_join(c[i], nullptr);
    for (int i = 0; i < 2; i++)

        pthread_join(p[i], nullptr);

    delete rq;
    return 0; } ```



运行结果

在这里插入图片描述

五、总结

互斥锁与信号量的异同

  • 互斥锁由同一线程加放锁,信号量可以由不同线程进行PV操作。

  • 计数信号量允许多个线程,且值为剩余可用资源数量。互斥锁保证多个线程对一个共享资源的互斥访问,信号量用于协调多个线程对一系列资源的访问条。

条件变量与信号量的异同

  • 使用条件变量可以一次唤醒所有等待者,而这个信号量没有的功能。

  • 信号量是有一个值,而条件变量是没有的。从实现上来说一个信号量可以是用mutex + count + cond实现的。因为信号量有一个状态,可以精准的同步,信号量可以解决条件变量中存在的唤醒丢失问题。

  • 条件变量一般需要配合互斥锁使用,而信号量可根据情况而定。

  • 有了互斥锁和条件变量还提供信号量的原因是:尽管信号量的意图在于进程间同步,互斥锁和条件变量的意图在于线程间同步,但是信号量也可用于线程间,互斥锁和条件变量也可用于进程间。信号量最有用的场景是用以指明可用资源的数量。

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

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

相关文章

Databend 源码阅读:配置管理

作者&#xff1a;尚卓燃&#xff08;PsiACE&#xff09;澳门科技大学在读硕士&#xff0c;Databend 研发工程师实习生 Apache OpenDAL(Incubating) Committer https://github.com/PsiACE 对于 Databend 这样复杂的数据库服务端程序&#xff0c;往往需要支持大量的可配置选项&am…

探索Lighthouse性能分数计算背后的奥秘

本文作者为 360 奇舞团前端开发工程师 作为开发我们都知道&#xff0c;页面性能很重要&#xff0c;一个性能良好的页面可以给用户带来非常好的用户体验。那么&#xff0c;怎么能知道自己写的页面性能是好是坏呢&#xff1f; Lighthouse 是Chrome提供给开发者用来测量页面性能的…

原生js之变量、作用域与内存

原生js之变量、作用域与内存 变量变量类型原始值引用值 复制值传递参数判断类型typeofinstanceof不常用的判断方法constructor构造方法判断Object.getPrototypeOf() 最好用的判断数据类型的方法!toString.call小总结 作用域链垃圾回收标记清理引用计数v8回收v8对GC的优化新生代…

对标8155体验,降本20%以上!这家企业用“量产”证明

智能座舱逐渐成为智能汽车标配。 根据高工智能汽车研究院监测的数据显示&#xff0c;2022年中国市场&#xff08;不含进出口&#xff09;乘用车搭载智能数字座舱&#xff08;大屏语音车联网OTA&#xff09;前装标配交付795.05万辆&#xff0c;同比增长40.59%&#xff0c;前装搭…

卫星通信链路预算(附程序)

文章目录 简介链路基本概念信息速率与传输速率载波占用带宽载波与噪声功率比 天线参数天线增益波束宽度极化损耗 有效全向辐射功率接收系统品质因数自由空间的传播损耗链路计算程序 简介 卫星通信链路是指从信源开始&#xff0c;通过编码调制及微波上变频、发射机和天线&#…

流量卡不带通话功能,这样的流量卡用着真的安全?

哈喽大家好&#xff0c;很多朋友私信小编&#xff0c;咨询关于纯流量卡的一些问题&#xff0c;可能大家都知道&#xff0c;现在正规的号卡也推出了“纯流量版”&#xff0c;也就是不带通话功能&#xff0c;那么&#xff0c;网上的带手机号的纯流量卡真的用着安全吗&#xff1f;…

企业数字化转型要怎么做?在线报修系统是如何解决维修难题的?

随着科技互联网等新技术的不断发展&#xff0c;企业服务已从信息化迈向数字化阶段。大型企业正迫切地向数字化转型&#xff0c;并需要技术革新和与云的融合&#xff0c;以应对当今市场的挑战。为满足这一需求&#xff0c;广东工程宝科技有限公司推出了一款新一代故障报修产品—…

如何快速搭建一个react项目?如何使用react脚手架快速搭建项目?

如何使用react脚手架快速搭建项目&#xff1f; 一、前提 电脑已经安装了node和npm环境。 react文档中要求Node > 8.10 和 npm > 5.6&#xff0c;查看版本&#xff1a;node -v&#xff1b;npm -v&#xff1b; 二、步骤 1、在合适的文件夹中打开命令行窗口cmd 2、全局安…

前端项目配置 prettier、eslint、lint-stages、husky、commitlint 、style-lint设置代码书写和提交规范

prettier中文网&#xff1a;Options Prettier 中文网 eslint中文网 &#xff1a;ESLint 中文网 github husky &#xff1a; https://github.com/typicode/husky commitlint.js 官网&#xff1a;commitlint - Lint commit messages 、github&#xff1a;GitHub - conventiona…

为什么大力推行国密算法SSL证书

国密算法SSL证书是一种采用我国自主研发的SM2公钥算法体系&#xff0c;支持SM2、SM3、SM4等国产密码算法及国密SSL安全协议的数字证书。相比于普通SSL证书&#xff0c;国密SSL证书具有以下特点&#xff1a; 自主可控&#xff1a;国密SSL证书采用我国自主研发的SM2公钥算法体系&…

十一,从摄像机打印HDR环境贴图

越来越接近真相了。我们很自然地想到&#xff0c;如果把漫游器放在中心打印&#xff0c;是不是就可以打印整个等距柱状投影图了呢&#xff1f;是的&#xff0c;但是&#xff0c;只是要注意的是&#xff0c;立方体贴图的内部和外部尽管一样&#xff0c;但是还是稍微有点模糊&…

Git 学习(2)

Git 学习&#xff08;2&#xff09; 版本号 Git 中文件的版本号是 40 位十六进制的数字字符串&#xff0c;采用 SHA-1 加密算法计算获得 这样一方面可避免在合并时的冲突问题 另一方面可以用于文件定位&#xff0c;其中前两位表示文件夹&#xff0c;后 38 位表示文件 指令介…

DDS信号发生器Verilog波形发生器FPGA

名称&#xff1a;DDS信号发生器Verilog波形发生器 软件&#xff1a;Quartus 语言&#xff1a;Verilog 要求&#xff1a; 1.可产生正弦波&#xff0c;锯齿波&#xff0c;三角波&#xff0c;方波4种波形&#xff0c;频率可调 2.具有波形选择、起动、停止功能。 代码下载&…

Flink on yarn 实战和源码分析

版本&#xff1a;1.13.6 目录 Flink on yarn 的3种模式的使用 yarn session 模式源码分析 yarn per-job模式源码分析 application模式源码分析 Flink on yarn 的3种模式的使用 Application Mode # ./bin/flink run-application -t yarn-application ./examples/streaming…

Apollo简易地图制作

在Apollo中模拟障碍物 一、准备工作 在模拟障碍物之前&#xff0c;需要下载并编译Apollo源码&#xff0c;过程可以依据Apollo开放平台文档&#xff0c;其中可能遇到的问题在这里或许可以寻找到答案 二、运行Dreamview 进入容器 cd ~/apollobash docker/scripts/dev_start.s…

515万新作者投身电商事业,抖音电商将投入更多资源扶持作者长期发展

9月27日&#xff0c;2023抖音电商作者峰会在上海举办。上千位抖音电商作者、MCN机构、精选联盟服务商、商家等重要生态伙伴参会&#xff0c;围绕大会主题“向新成长”进行了深入探讨。会上&#xff0c;抖音电商总裁魏雯雯提到&#xff0c;电商作者的事业有更多发展方向。为助力…

输送机使用的常见误区

输送机也称流水线&#xff0c;是指在自动化生产过程中起到运输货物&#xff0c;联通各个生产设备的主要机械设备。但在使用的过程中&#xff0c;很多用户对于输送机的使用存在一定的误区&#xff0c;导致设备故障频出&#xff0c;下面就针对用户已在使用输送机过程中的常见误区…

以太网中的介质共享访问控制机制

什么是CSMA/CD CSMA/CD&#xff08;Carrier Sense Multiple Access with Collision Detection&#xff09;是一种用于以太网等共享介质的访问控制机制。它用于协调多个设备共享同一物理介质&#xff08;例如同一局域网&#xff09;上的传输权利&#xff0c;以避免碰撞并提供公…

mac安装python2

Python 2 于 2020 年 1 月 1 日宣布结束支持&#xff0c;包括 Homebrew 在内的许多项目和包管理器已经停止支持 Python 2。 如果现在你还要安装 Python 2&#xff0c;需要从 Python 官网下载安装包&#xff1a; 访问 Python 的发布页面。从页面底部找到 Python 2 的最后一个版…

tp8 Editor.md

Editor.md - 开源在线 Markdown 编辑器 放于public文件夹下 html代码&#xff1a; <div class"layui-col-md12" id"content"><textarea name"content" placeholder"详情" class"layui-textarea">{notempty nam…