【Linux】线程篇Ⅱ:线程的同步与互斥(锁)接口及使用、线程库及线程id的理解


🔗接上篇【线程篇Ⅰ】

👉🔗【Linux】线程篇Ⅰ:线程和task_struct 执行流的理解、相关接口命令、线程异常、线程的私有和共享


五、线程库 和 线程 id

对于 Linux 目前实现的 NPTL 实现而言

pthread_t 类型的线程 ID,本质就是一个 进程地址空间 上的一个地址。

线程篇Ⅰ中涉及到的接口,主要是原生系统库的系统级解决方案,虽然在库中实现但是跟语言一样,比语言更靠近底层罢了。而 C++ 其实是对线程库做的封装!!

虽然原生接口效率更高,但是 语言接口 有跨平台性。不确定只在 Linux 下跑的还是推荐使用语言接口。

在这里插入图片描述

另:有如下 线程独立栈 的理解和应用
在这里插入图片描述

  • 通过更改 寄存器 ebp、esp 就能 切换 线程栈
  • 数据通过 ebp - 偏移量 进行访问或者开辟空间(ebp 是一个相对稳定的位置)
  • 首地址之所以是低地址,是因为栈的扩展方式 和 ebp 开辟空间的方式(如图)。
/*__thread*/ int g_val = 100;

void *threadRoutine(void* args)
{
    string name = static_cast<const char*>(args);
    int cnt = 5;
    while(cnt)
    {
    	// 局部变量
        cout << name << " : " << cnt-- << " : " << hexAddr(pthread_self()) << " &cnt: " << &cnt << endl;
        // 全局变量
        cout << name << " g_val: " << g_val++ << ", &g_val: " << &g_val << endl;
        sleep(1);
	}
	return nullptr;
}

int main()
{
	pthread_t t1, t2, t3;
	pthread_create(&t1, nullptr, threadRoutine, (void*)"thread 1");
	pthread_create(&t2, nullptr, threadRoutine, (void*)"thread 2"); 
	
	pthread_join(t1, nullptr);
	pthread_join(t2, nullptr);
	return 0
}
  • 线程函数中的 临时变量,储存在 进程地址空间 共享区的 线程库的 线程栈 中,线程各自使用互不影响。

  • 全局变量 储存在主线程的 已初始化数据段,其他新线程访问全局变量访问的是同一个,是并发访问。

  • 前面 声明 __thread (局部存储)字样的全局部变量 ,储存在已初始化数据段,并在产生新线程后,拷贝到 线程库的 线程局部存储段 中,供各自线程使用且互不影响。(由于地址空间的分布规则,全局数据被拷贝后的地址会比原来的地址大很多,如上图示)

      __thread 定义的全局变量 可以应用在:
      		带出某一个函数 被 各个线程调用的次数
    
  • __thread 局部存储 与 static 静态变量 没有关系哦,静态变量被所有线程共享的,存在已初始化数据段。


六、Linux 线程互斥(加锁)

任何一个时刻,都只允许一个执行流在进行共享资源的访问,叫做 互斥,也叫 加锁

  • 我们把任何一个时刻,都只允许一个执行流在进行访问的 共享资源,叫做 临界资源
  • 任何一个线程,都有代码 访问临界资源 的叫做,临界区
  • 不访问临界资源 的区域叫做,非临界区
  • 控制进出临界区的手段(加锁)造就了临界资源。

加锁 可以保证一系列操作,要不做完 要不不做,这种特性叫做 原子性,临界资源是有原子性的。

1. 一些接口

pthread_mutex_t 是原生系统库给我们提供的一种数据类型,用来创建锁。

以下接口头文件相同:

 #include <pthread.h>

1.1 pthread_mutex_init 函数:锁的初始化

int pthread_mutex_init(pthread_mutex_t *restrict mutex, const pthread_mutexattr_t *restrict attr);

参数 restrict mutex:

  • 需要初始化的锁名称

参数 restrict attr:

  • 属性,保持默认设置为 nullptr

注意:静态或者全局的锁,可以用如下的宏直接对锁做初始化

pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;

1.2 pthread_mutex_destroy 函数:锁的销毁

int pthread_mutex_destroy(pthread_mutex_t *mutex);

参数 mutex:

  • 需要销毁的锁的名称

1.3 pthread_mutex_lock 函数:上锁(阻塞)

锁不上就阻塞,直到能锁上,再进行下一步操作。

int pthread_mutex_lock(pthread_mutex_t *mutex);

参数 mutex:

  • 需要上锁的锁的名称
pthread_mutex_trylock 函数:尝试上锁(非阻塞)

尝试锁一下,锁不到就拉倒,不会影响自己进行下一步操作。

int pthread_mutex_trylock(pthread_mutex_t *mutex);

参数 mutex:

  • 需要尝试上锁的锁的名称

1.4 pthread_mutex_unlock 函数:解锁

int pthread_mutex_unlock(pthread_mutex_t *mutex);

参数mutex:

  • 需要解锁的锁的名称

1.5 使用案例 及 注意细节

🌰案例:实现多线程同时抢票

// 临界资源
int tickets = 1000;                               

class TData
{
public:
    TData(const string &name, pthread_mutex_t *mutex):_name(name), _pmutex(mutex)
    {}
    ~TData()
    {}
public:
    string _name;
    pthread_mutex_t *_pmutex;
};

void threadRoutine(void *args)
{
    TData *td = static_cast<TData *>(args);
    while (true)
    {
        pthread_mutex_lock(td->_pmutex); 
        if (tickets > 0)
        {
            usleep(2000);
            cout << td->_name << " get a ticket: " << tickets-- << endl; // 临界区
            pthread_mutex_unlock(td->_pmutex);
        }
        else
        {
	        pthread_mutex_unlock(td->_pmutex);
            break;
        }
        // 我们抢完一张票的时候,我们还要有后续的动作
        // usleep(13);
    }
}

int main()
{
    pthread_mutex_t mutex;
    pthread_mutex_init(&mutex, nullptr);

    pthread_t tids[4];
    int n = sizeof(tids)/sizeof(tids[0]);
    for(int i = 0; i < n; i++)
    {
        char name[64];
        snprintf(name, 64, "thread-%d", i+1);
        TData *td = new TData(name, &mutex);
        pthread_create(tids+i, nullptr, threadRoutine, td);
    }

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

    pthread_mutex_destroy(&mutex);
	return 0;
}

注意细节:

  1. 加锁本质就是给 临界区 加锁,凡是访问同一个 临界资源 的线程,都要进行加锁保护,而且 必须加同一把锁。加锁的粒度 要尽可能的细

  2. 由于所有线程都必须要先看到同一把锁,锁本身就是公共资源,不过 加锁和解锁本身就是原子的,可以保证自己的安全

  3. 临界区可以是一行代码,可以是一批代码,线程仍然可能在临界区任意位置被切换,加锁并不影响这一点。

    切换线程不会影响锁的安全性,比如该线程加锁后被切走了,由于这个整个临界区的原子性,没有被解锁的情况下,任何人都没有办法进入临界区。即 他人无法成功的申请到锁,因为锁被该线程拿走了。

  4. 这也正是体现互斥带来的串行化的表现,站在其他线程的角度,对其他线程有意义的状态就是:锁被我申请(持有锁),锁被我释放了(不持有锁), 原子性就体现在这

2. 原理

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

  2. 寄存器硬件只有一套,但是寄存器内部的数据是每一个线程都要有的。寄存器 != 寄存器的内容(执行流的上下文)

  3. 加锁解锁的代码怎么执行的?

  • 首先:在内存中创建锁对象 mutex,这是一个共享对象,里面置 1

    • 创建锁对象只会生成一个 1,1 在哪个线程的手上就是哪个线程有权访问临界资源,这个 1 只会流转
  • 接着,线程调用接口 pthread_mutex_lock()

// 实现接口,编译的伪代码如下
lock:
	movb $0, %al	// al是放线程上下文的寄存器,这里调用了线程,向自己的上下文写入 0
	xchgb %al, mutex	// 将al寄存器和内存中mutex里的值做交换(本质是,线程将共享数据交换到自己私有的上下文中,因为这里只有一条代码,正是这一条代码才保证了加锁的原子性)
	if(al寄存器的内容 > 0)
		return 0;
	else
		挂起等待;
	goto lock;
		
  • 最后调用接口 pthread_mutex_unlock()
	movb $1, %mutex	// 这里直接置 1,而不是 exchange,也意在解锁可以由其他线程完成!
	唤醒等待 Mutex 的线程;
	return 0;

3. 死锁

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

死锁四个必要条件:

  • 互斥:一个资源每次只能被一个执行流使用
  • 请求与保持:一个执行流因请求资源而阻塞时,对已获得的资源保持不放
  • 不剥夺:一个执行流已获得的资源,在末使用完之前,不能强行剥夺
  • 循环(环路)等待:若干执行流之间形成一种头尾相接的循环等待资源的关系

避免死锁:

  • 破坏死锁的任一必要条件
  • 主动释放锁
  • 加锁顺序一致
  • 资源一次性分配

避免死锁算法:

  • 死锁检测算法
  • 银行家算法

4. 自旋锁

线程自旋或者挂起等待,由访问临界区要花费的时间来决定。需要结合具体的场景。

  • 自旋就是轮询

  • 自旋锁接口为 pthread_spin_lock

    int pthread_spin_lock(pthread_spinlock_t *lock);
    

    解锁接口为 pthread_spin_unlock

    int pthread_spin_unlock(pthread_spinlock_t *lock);
    
  • 初始化接口为 pthread_spin_init

    int pthread_spin_init(pthread_spinlock_t *lock, int pshared);		// 第二个参数为属性 默认设置为 0 即可
    

    销毁接口为pthread_spin_destroy

    int pthread_spin_destroy(pthread_spinlock_t *lock);
    

七. Linux 线程同步

在安全的规则下,让线程能够按照某种特定的顺序访问临界资源,从而有效避免饥饿问题,这就叫线程同步!


1. 条件变量 的一些接口

条件变量,允许线程在 cond 中队列式等待(就是一种顺序)

头文件:

#include <pthread.h>

1. 1 pthread_cond_init 函数:条件变量 初始化

int pthread_cond_init(pthread_cond_t *restrict cond,constpthread_condattr_t *restrict attr); 

参数 cond:

  • 要初始化的条件变量

参数 attr

  • NULL

注意:可以用如下的宏直接对条件变量做初始化

pthread_cond_t cond = PTHREAD_COND_INITIALIZER;

1. 2 pthread_cond_destroy 函数:销毁 条件变量

int pthread_cond_destroy(pthread_cond_t *cond)

参数 cond:

  • 要销毁的条件变量

1. 3 pthread_cond_wait 函数:等待条件满足

int pthread_cond_wait(pthread_cond_t *restrict cond, pthread_mutex_t *restrict mutex);

参数 cond:

  • 要在这个条件变量上等待

参数 mutex:

  • 互斥量(因为要让线程休眠等待,不能持锁等待,注定了 pthread_cond_wait 要有锁的释放的能力)

1. 4 pthread_cond_signal / broadcast 函数:唤醒等待

唤醒全部:

int pthread_cond_broadcast(pthread_cond_t *cond);

唤醒(第)一个:

int pthread_cond_signal(pthread_cond_t *cond);

2. 生产者消费者模型(CP模型)

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

这样的模型满足了,解耦、支持并发、支持忙闲不均的优点。

三种关系:

  • 生产者 & 生产者 -> 互斥关系
  • 消费者 & 消费者 -> 互斥关系
  • 生产者 & 消费者 -> 同步 + 互斥关系

两种角色:

  • 生产者和消费者

一个交易场所:

  • 缓冲区(通常是)

3. blockingQueue 阻塞队列

在多线程编程中阻塞队列(Blocking Queue)是一种常用于实现生产者和消费者模型的数据结构。其与普通的队列区别在于,当队列为空时,从队列获取元素的操作将会被阻塞,直到队列中被放入了元素;当队列满时,往队列里存放元素的操作也会被阻塞,直到有元素被从队列中取出(以上的操作都是基于不同的线程来说的,线程在对阻塞队列进程操作时会被阻塞)

使用举例

blockQueue.hpp

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

const int gcap = 5;	// 阻塞队列的容量默认为 5

// 阻塞队列中放一个个 task 对象
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);
    }
    bool isFull(){ return _q.size() == _cap; }
    bool isEmpty() { return _q.empty(); }
    
    void push(const T &in)	// 生产者 push
    {
        pthread_mutex_lock(&_mutex);
        // 细节1:一定要保证,在任何时候,都是符合条件,才进行生产,所以需要 while 判断,以免被误唤醒
        while(isFull()) // 1. 我们只能在临界区内部,判断临界资源是否就绪!注定了我们在当前一定是持有锁的!
        {
            // 2. 要让线程进行休眠等待,不能持有锁等待!
            // 3. 注定了,pthread_cond_wait要有锁的释放的能力!
            pthread_cond_wait(&_productorCond, &_mutex); 
            // 4. 当线程醒来的时候,注定了继续从临界区内部继续运行!因为线程是在临界区被切走的!
            // 5. 也就是说当线程被唤醒的时候,继续在 pthread_cond_wait 函数出向后运行,又要重新申请锁,申请成功才会彻底返回
            // 其实... 这里面的内容我们都可以不关心,接口自会保证线程安全
        }
        // 没有满的,就要让他进行生产
        _q.push(in);
        // if(_q.size() >= _cap/2) // 可以加策略
        pthread_cond_signal(&_consumerCond);	
        pthread_mutex_unlock(&_mutex);
        // pthread_cond_signal(&_consumerCond);
    }
    
    void pop(T *out)	// 消费者 pop
    {
        pthread_mutex_lock(&_mutex);
        while(isEmpty()) 
        {
            pthread_cond_wait(&_consumerCond, &_mutex);
        }
        *out = _q.front();
        _q.pop();
        // 可以自行添加加策略
        pthread_cond_signal(&_productorCond);
        pthread_mutex_unlock(&_mutex);
    }
    ~BlockQueue()
    {
        pthread_mutex_destroy(&_mutex);
        pthread_cond_destroy(&_consumerCond);
        pthread_cond_destroy(&_productorCond);
    }
private:
    std::queue<T> _q;
    int _cap;
    // 我们生产和消费访问的是同一个 queue&&queue 被当做整体使用!所以可以只用一把锁!!
    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 = -2;
            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 <pthread.h>
#include <unistd.h>
#include <ctime>

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

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

int main()
{
    srand((uint64_t)time(nullptr) ^ getpid());
    // BlockQueue<int> *bq = new BlockQueue<int>();
    BlockQueue<Task> *bq = new BlockQueue<Task>();
    // 单生产和单消费 -> 多生产和多消费
    pthread_t c[2], p[3];
    pthread_create(&c[0], nullptr, consumer, bq);
    pthread_create(&c[1], nullptr, consumer, bq);
    pthread_create(&p[0], nullptr, productor, bq);
    pthread_create(&p[1], nullptr, productor, bq);
    pthread_create(&p[2], nullptr, productor, bq);

    pthread_join(c[0], nullptr);
    pthread_join(c[1], nullptr);
    pthread_join(p[0], nullptr);
    pthread_join(p[1], nullptr);
    pthread_join(p[2], nullptr);
    delete bq;
    return 0;
}

4. 信号量

  • 信号量是资源的可访问计数器,申请信号量成功,本身就表明资源可用
  • 用申请信号量失败本身表明资源不可用

本质就是把判断转化成为信号量的申请行为。

创建信号量的类型:sem_t

接口头文件:

#include <semaphore.h>

4.1 sem_init 函数:初始化信号量

int sem_init(sem_t *sem, int pshared, unsigned int value);

参数:

  • pshared:0 表示线程间共享,非零表示进程间共享
  • value:信号量初始值

4.2 sem_destroy 函数:销毁信号量

int sem_destroy(sem_t *sem);

4.3 sem_wait 函数:等待信号量

功能:等待信号量,会将信号量的值减1

int sem_wait(sem_t *sem); //P()

4.4 sem_post 函数:发布信号量

发布信号量,表示资源使用完毕,可以归还资源了。将信号量值加1。

int sem_post(sem_t *sem);//V()

5. 基于环形队列的 PC 模型

  1. 生产者向 tail push 数据,他只关心空间
    消费者向 head pop 数据,关心数据

  2. 只要信号量不为 0,表示资源可用,表示线程可访问

  3. 只有为空和为满的时候,cp才会指向同一个位置

  4. 环形队列只要我们访问不同的区域,生产和消费行为可以同时进行

生产者伪代码如下:

sem_room: N
P(sem_room) // 申请空间信号量
进行生产活动
V(sem_data)

消费者伪代码如下:

sem_data: O
P(sem_data) // 申请数据信号量
进行消费活动
V(sem_room)

使用举例

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. 什么时候用锁,什么时候用sem?你对应的临界资源,是否被整体使用!
        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>
#include <unistd.h>

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 = -2;
            else
                _result = _x % _y;
        }
        break;
        default:
            break;
        }

        usleep(100000);
    }

    // 任务内容
    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);

    // 多生产,多消费,该如何更改代码呢?done
    // 意义在哪里呢?意义绝对不在从缓冲区冲放入和拿去,意义在于,放前并发构建Task,获取后多线程可以并发处理task,因为这些操作没有加锁!
    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;
}

5. 锁 和 信号量 总结

什么时候用锁,什么时候用信号量呢?

  • 临界资源被整体使用时(例如上述的 BlockQueue),使用锁
  • 临界资源可以被拆开使用时(例如上述的 RingQueue),使用信号量


🥰如果本文对你有些帮助,请给个赞或收藏,你的支持是对作者大大莫大的鼓励!!(✿◡‿◡) 欢迎评论留言~~


  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值