线程的同步与互斥


互斥和同步的相关背景概念

临界资源:能被多线程执行流共享访问的资源
临界区:线程内部,访问临界资源的代码
原子性:一件事情要么不执行,如果执行的话,一直执行完毕
互斥:任意时刻只允许一个线程进入临界区访问临界资源
同步:访问临界资源的过程,在安全的前提下,让访问临界资源具有合理性。


为什么会存在线程互斥?
多个线程对临界资源进行争抢访问时可能会造成数据二义,因此需要保证任意时刻只有一个线程能够访问资源,从而实现线程访问临界资源的安全性。

Linux线程互斥

互斥量

大部分情况,线程使用的数据都是局部变量,变量的地址在线程栈空间内,这种情况,变量归属于单个线程,其他线程无法获得这种变量。但有的时候很多变量需要在线程间共享,这样的变量称为共享变量,可以通过数据的共享完成线程之间的交互。多个线程并发的操作共享变量,会带来一些问题。

简单模拟一个抢票系统
#include <iostream>
#include <unistd.h>
#include <pthread.h>
#include <string>
#include <stdlib.h>
using namespace std; 

class Ticket
{
public:
    Ticket()
        :_tickets(1000)
    {}
    bool GetTicket()
    {
        if(_tickets > 0)
        {
            usleep(10000);
            _tickets--;
            cout << "我是线程[" << pthread_self() << "], " << "买票成功,剩余票数:"<< _tickets << endl;
            return true;
        }
        else
        {
            cout << "票已被抢空" << endl;
            return false;
        }
        return true;
    }
private:
    int _tickets;
};

void* ThreadRoutine(void* arg)
{
    Ticket* tck = (Ticket*)arg;
    while(tck->GetTicket()); //不断在抢票
} 

int main(void)
{
    Ticket* t = new Ticket;
    //创建5个新线程
    pthread_t pt[5]; 
    for(int i = 0; i < 5; i++)
    {
        pthread_create(pt + i, NULL, ThreadRoutine, (void*)t);
    }
    for(int i = 0; i < 5; i++)
    {
        pthread_join(pt[i], NULL);
    }
    return 0;
}

在这里插入图片描述
票都已经被抢空了,居然还能买票,这显然和我们的预期是不一样的。
为什么会存在这种情况?
在这里插入图片描述
上面代码的临界区是

if(_tickets > 0)
        {
            usleep(10000);
            _tickets--;
            cout << "我是线程[" << pthread_self() << "], " << "买票成功,剩余票数:"<< _tickets << endl;
            return true;
        }
  • if判断以后线程可能会被换下,系统调度其他线程使用CPU。
  • usleep是一个模拟漫长业务的过程,在这个过程中,可能发生了多次线程切换,多个线程进入该代码段。
  • – – tickets这个操作不是原子性的,在汇编级别,它是多行代码。在它还没有执行完毕的时候,线程可能会被切换。

– – 对应的几句汇编指令
load :将共享变量ticket从内存加载到寄存器中
update : 更新寄存器里面的值,执行-1操作
store :将新值,从寄存器写回共享变量ticket的内存地址
在这里插入图片描述
但是线程会按一定的限制不断地切换上下文,所以实际可能是下面这样的:
在这里插入图片描述
解决上面问题的需要做到如下三点:

  • 1、线程必须互斥:当代码进入临界区时,不允许其他线程进入临界区。
  • 2、如果多个线程同时要求执行临界区的代码时,并且此时临界区没有线程在执行,那么允许一个线程进入。
  • 3、如果线程不在临界区执行,那么这个线程就不能阻止其他线程进入临界区。

能够做到上述三点的方法就是加锁,Linux上提供的锁称为互斥量

在这里插入图片描述

互斥量的接口

创建一把互斥量。
pthread_mutex_t name
Linux上锁的本质实际上是一个pthread_mutex_t类型的变量,name是锁名。

互斥量可以简单地就是一个0/1的计数器,用于标记资源访问状态:

  • 0表示已经有执行流加锁成功,其他线程不可访问资源。
  • 1表示未加锁,资源可以访问。

初始化互斥量
初始化方法一:静态分配,给一个宏常量PTHREAD_MUTEX_INITIALIZER.

//例如
pthread_mutex_t mtx = PTHREAD_MUTEX_INITIALIZER

初始化方法二:动态分配,使用函数pthread_mutex_init

#include <pthread.h>
int pthread_mutex_init(pthread_mutex_t *restrict mutex,const pthread_mutexattr_t *restrict attr);

参数
mutex:需要初始化的互斥量。
attr:这里可以设置为NULL,表示使用默认互斥属性。
返回值:成功返回0,失败返回错误码。


销毁互斥量

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

参数
mutex:要销毁的互斥量。
返回值:成功返回0,失败返回错误码。

【注意】

  • 使用宏PTHREAD_MUTEX_INITIALIZER初始化的互斥量不需要销毁
  • 要确保后面不会再加锁,再去销毁互斥量
  • 不要销毁一个已经加锁但还没解锁的互斥量。

互斥量加锁和解锁

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

参数:要加锁/解锁的互斥量。
返回值:成功返回0,失败返回错误码。

  • 准备加锁的互斥量,处于未锁状态,那么就会加锁成功。
  • 准备加锁的互斥量,之前已被锁定,那么线程调用pthread_mutex_lock会被挂起等待,等待互斥量解锁。

对于互斥量、加锁、解锁,一种抽象假设(仅仅用于我自己的理解和记忆):
有一个房间,里面存放着一份资源。刚开始的时候房间有一个入口,但是入口只有一个门框,其他的线程都可以进去享用资源。创建一个互斥量(锁),相当于给这个房间定制了了一个门(这个门只能从里面打开,外面无法打开)。但房间内资源只有一份,所以一个线程把门锁了(加锁)自己享用房间里的资源,其他线程就就进不去了,只能在门外等那个进去的线程开门出来(解锁),然后其他线程才能去把门锁上享用资源。

加锁改进抢票系统

#include <iostream>
#include <unistd.h>
#include <pthread.h>
#include <string>
#include <stdlib.h>
using namespace std; 

class Ticket
{
public:
    Ticket()
        :_tickets(1000)
    {
        pthread_mutex_init(&_mtx, nullptr);
    }
    bool GetTicket()
    {
        pthread_mutex_lock(&_mtx); //加锁
        if(_tickets > 0)
        {
            usleep(10000);
            _tickets--;
            cout << "我是线程[" << pthread_self() << "], " << "买票成功,剩余票数:"<< _tickets << endl;

            //解锁
            pthread_mutex_unlock(&_mtx);
            return true;
        }
        else
        {
            cout << "票已被抢空" << endl;
            //解锁
            pthread_mutex_unlock(&_mtx);
            return false;
        }

    }
    ~Ticket()
    {
        pthread_mutex_destroy(&_mtx);
    }
private:
    int _tickets;
    pthread_mutex_t _mtx; //创建一把锁(互斥量),全局只有一个
};

void* ThreadRoutine(void* arg)
{
    Ticket* tck = (Ticket*)arg;
    while(tck->GetTicket()); //不断在抢票
} 

int main(void)
{
    Ticket* t = new Ticket;
    //创建5个新线程
    pthread_t pt[5]; 
    for(int i = 0; i < 5; i++)
    {
        pthread_create(pt + i, NULL, ThreadRoutine, (void*)t);
    }
    for(int i = 0; i < 5; i++)
    {
        pthread_join(pt[i], NULL);
    }
    return 0;
}

运行结果最后部分片段,票被抢空后,没有再出现继续抢票的行为。
在这里插入图片描述
现在我们已经知道,多个线程对临界资源进行争抢访问可能会造成数据的二义,我们的解决办法是加锁。
但仔细想想,锁也是临界资源,按前面所述的知识我们认为加锁和解锁的过程可能不是原子性的,那么它就可能不是安全的,但它只有保证了自己的安全,才能去保护其他临界资源的安全性。所以它怎么保证自己的安全?

为了实现互斥锁的操作,大多数体系结构都提供了swapexchange指令,它们的作用是将寄存器和内存单元中的数据交换,由于只有一条指令,保证了这步操作的原子性。即使发生上下文切换,也只有已经交换完成和还没有开始。加锁和解锁的伪代码如下:
在这里插入图片描述
【注意】
为了保证临界区的安全,必须保证每个线程都必须遵守相同的编码规范(例如,一个线程申请了锁,那么其他线程也必须申请)。

可重入 VS 线程安全

重入:同一个函数被不同执行流调用,当前执行流还没执行完,另外一个执行流就再次进入。一个函数在重入的情况下,如果没有出现任何问题,则称这个函数是可重入函数;否则,称为不可重入函数
线程安全
线程安全:在没有锁的保护下,多个线程并发执行同一段代码,不会出现不稳定和非预期的结果。

【常见不可重入情况】

  • 调用了malloc、free函数,因为malloc函数是用全局链表管理堆的
  • 调用了标准I/O函数
  • 函数内使用了静态的数据结构

【常见可重入】

  • 不使用全局变量或静态变量
  • 不使用malloc、new函数开辟出来的空间
  • 不调用不可重入函数
  • 不返回静态和全局数据
  • 使用本地数据,通过制作全局数据的本地拷贝来保护全局数据。

可重入与线程安全的联系

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

可重入与线程安全区别

  • 可重入是线程安全的一种
  • 线程安全不一定是可重入,可重入一定是安全的
  • 如果对临界资源的访问加上锁,则函数是线程安全的。如果重入函数锁未释放会产生死锁,因此不可重入。

死锁

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

如果造成了死锁,那么一定满足下列要求

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

如何避免死锁?
1、破坏死锁的四个必要条件
2、加锁顺序一致
3、避免锁未释放的场景
4、资源一次性分配

避免死锁的两种常见算法

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

Linux线程同步

条件变量

多线程互斥地访问临界资源的时候,有了锁,除了正在使用临界资源的线程外,其他线程什么也做不了,只能等待。这种情况确实拉低了效率,因为我们不知道临界资源的状态,此时最希望的就是其他线程去做自己的事情,当使用临界资源的线程使用完后,通知其他线程,其他线程再来竞争使用临界资源。可以达到这个目的的手段就是条件变量

条件变量用于多线程间关于临界资源状态变化的通信。当出现一个线程访问临界资源的行为依赖于其他线程时就可以使用条件变量。

同步的概念:在保证数据安全的前提下,让临界资源的访问具有合理性,从而有效避免饥饿问题。
竞态条件:因为时序问题,从而导致程序异常。

创建条件变量
pthread_cond_t name
pthread_cond_t 条件变量的类型名称
name:条件变量的变量名


初始化条件变量

  • 方法一:静态初始化。使用宏PTHRAEAD_COND_INITIALIZER
pthread_cond_t cond = PTHREAD_COND_INITIALIZER;
  • 方法二:动态初始化。使用函数pthread_cond_init
#include <pthread.h>
int pthread_cond_init(pthread_cond_t *restrict cond,
              const pthread_condattr_t *restrict attr);

参数
cond:要初始化的条件变量。
attr:这里可以设置为nullptr,表示使用默认属性。
返回值:成功返回0,失败返回错误码。


销毁条件变量

#include <pthread.h>
int pthread_cond_destroy(pthread_cond_t *cond);

参数:要销毁的条件变量。
返回值:成功返回0,失败返回-1。


等待条件变量的条件满足

#include <pthread.h>
int pthread_cond_wait(pthread_cond_t *restrict cond,
              pthread_mutex_t *restrict mutex);

参数
cond:条件变量
mutex:互斥量


唤醒等待

#include <pthread.h>

int pthread_cond_broadcast(pthread_cond_t *cond); //唤醒一批线程
int pthread_cond_signal(pthread_cond_t *cond); //唤醒一个线程

有一个关于该条件变量的等待队列,队列里的成员都是在该条件变量下等待的线程。如果使用的是pthread_cond_signal唤醒的是这个等待队列放在头部的线程;如果使用的是pthread_cond_broadcast唤醒的则是等待队列里所有的线程。这样可以达到按照一定的时序访问资源。

例如,完成两个线程通过条件变量实现交替打印的控制

#include <iostream>
#include <string>
#include <pthread.h>
#include <unistd.h>
#define NUM 4

pthread_mutex_t mtx;
pthread_cond_t cond;

void* ThreadCtrl(void* arg)
{
    while(true)
    {
        pthread_cond_signal(&cond);
        sleep(1);
    }
}
void* work(void* arg)
{
    std::string str = (char*)arg;
    while(true)
    {
        pthread_cond_wait(&cond, &mtx);
        std::cout << "我是线程" << str << std::endl;
    }
}


int main(void)
{
    pthread_mutex_init(&mtx, nullptr);
    pthread_cond_init(&cond, nullptr);

    pthread_t A, B, Ctrl;

    pthread_create(&A, nullptr, work, (void*)"A");
    pthread_create(&B, nullptr, work, (void*)"B");
    pthread_create(&Ctrl, nullptr, ThreadCtrl, (void*)"Ctrl");

    pthread_join(A, nullptr);
    pthread_join(B, nullptr);
    pthread_join(Ctrl, nullptr);
    
    pthread_mutex_destroy(&mtx);
    pthread_cond_destroy(&cond);
    return 0;
}

在这里插入图片描述

为什么pthread_cond_wait需要和互斥锁配套使用?
在这里插入图片描述
pthread_cond_wait和互斥锁配套使用的过程,先加锁,如果发现不满足该条件就解锁,然后再等待条件变量,pthread_cond_wait返回时,也就是条件满足时,自动去竞争锁。

生产者和消费者模型

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

在这里插入图片描述
生产者和消费者不直接通信,减少了双方的交易成本,提高了效率。

生产者和消费者之间的关系。
生产者和生产者:竞争、互斥
消费者和消费者:竞争、互斥
生产者和消费者:同步、互斥

基于阻塞队列的生产者和消费者模型

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


现在开始生产者和消费者模型的代码实现
所创建文件以及makefile文件内容

  • BlockQueue.hpp
  • CPer.cpp
  • Makefile

makefile文件内容:

cper : CPer.cpp
    g++ -o $@ $^ -lpthread

.PHONY:clean
clean:
    rm -f cper

首先搭好基本逻辑框架

BlockQueu.hpp文件

#pragma once
#include <queue>
//.hpp文件,可以声明和定义放在一起
//阻塞队列(交易场所)
namespace ns_blockqueue
{
    template <class T>
    class BlockQueue
    {
    public:
        BlockQueue(int cap = 5)
            :_capacity(cap)
        {}
    public:
        void push(const T& data)
        {
            //生产函数
        }
        void pop(T* out)
        {
            //消费函数
        }
    private:
        std::queue<T> _bq;
        int _capacity; //队列容量上限
    };
}

CPer.cpp文件

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

using namespace std;
#include "BlockQueue.hpp"
using namespace ns_blockqueue;

//消费者行为
void* consume(void* arg)
{
    BlockQueue<int> *bq = (BlockQueue<int>*)arg;
    //不断地从阻塞队列里拿取数据
    while(true)
    {
        int data = 0;
        sleep(2);
        bq->pop(&data);
        std::cout << "消费者线程拿到数据: " << data << std::endl;

    }
}
//生产者行为
void* product(void* arg)
{
    BlockQueue<int> *bq = (BlockQueue<int>*)arg;
    //不断制造数据,并放入阻塞队列
    while(true)
    {
        int data = rand() % 20 + 1;
        std::cout << "生产者线程成产数据" << data << ", 放入阻塞队列" << std::endl;
        bq->push(data);
        sleep(2);
    }
}

int main(void)
{
    srand((long long)time(nullptr));
    //创建一个交易场所(阻塞队列)
    BlockQueue<int> *bq = new BlockQueue<int>;

    pthread_t c, p;
    pthread_create(&c, nullptr, consume, (void*)bq); 
    pthread_create(&c, nullptr, product, (void*)bq); 
    //主线程等待
    pthread_join(c, nullptr);
    pthread_join(p, nullptr);
    return 0;
}

多线程编程中,这里往阻塞队列里push和pop数据,线程不安全,所以需要引入互斥量(锁)。在BlockQueue中增加了锁的创建、初始化、销毁、加锁和解锁。

#pragma once
#include <queue>
#include <pthread.h>
//.hpp文件,可以声明和定义放在一起
//阻塞队列(交易场所)
namespace ns_blockqueue
{
    template <class T>
    class BlockQueue
    {
    public:
        BlockQueue(int cap = 5)
            :_capacity(cap)
        {
            pthread_mutex_init(&mtx, nullptr);
        }
    public:
        void push(const T& data)
        {
            //生产函数
            
        }
        void pop(T* out)
        {
            //消费函数
        }
        ~BlockQueue()
        {
            pthread_mutex_destroy(&mtx);
        }
    private:
         void LockQueue()
        {
            pthread_mutex_lock(&mtx);
        }
        void UnLockQueue()
        {
            pthread_mutex_unlock(&mtx);
        }
    private:
        std::qeue<T> _bq;
        int _capacity; //队列容量上限
        pthread_mutex_t mtx;
    };
}

引入锁之后,开始实现阻塞队列的生产函数和消费函数

        void push(const T& data)
        {
            //生产函数
            LockQueue();
            _bq.push(data);
            UnLockQueue();
        }
        void pop(T* out)
        {
            //消费函数
            LockQueue();
            *out = _bq.front();
            _bq.pop();
            UnLockQueue();
        }

到这里,代码已经可以正常地跑出我们期望的样子了
在这里插入图片描述
如果把生产者生产的慢,而消费者一直在不断地消费。或者生产者生产地快而消费者消费的慢,这时候就出现问题了。
只有生产者知道消费者什么时候可以消费,也只有消费者知道生产者什么时候该生产了(生产者生产成功,消费者就可以消费;消费者消费成功,成产者就可以生产),很明显生产者线程访问临界资源的行为依赖于消费者的行为,所以可以引入一个条件变量,而消费者访问临界资源的行为也依赖于生产者的行为,所以一共需要引入两个条件变量。在BlockQueue中增加了两个条件变量的创建、初始化、销毁、等待和唤醒。让生产者线程在一个条件变量的阻塞队列中等待,让消费者在另一个条件变量的阻塞队列中等待。

namespace ns_blockqueue
{
    template <class T>
    class BlockQueue
    {
    public:
        BlockQueue(int cap = 5)
            :_capacity(cap)
        {
            pthread_mutex_init(&mtx, nullptr);
            pthread_cond_init(&_is_full, nullptr);
            pthread_cond_init(&_is_empty, nullptr);
        }
    public:
        void push(const T& data)
        {
            //生产函数
            LockQueue();
            _bq.push(data);
            UnLockQueue();
        }
        void pop(T* out)
        {
            //消费函数
            LockQueue();
            *out = _bq.front();
            _bq.pop();
            UnLockQueue();
        }
        ~BlockQueue()
        {
            pthread_mutex_destroy(&mtx);
            pthread_cond_destroy(&_is_full);
            pthread_cond_destroy(&_is_empty);
        }
    private:
        void LockQueue()
        {
            pthread_mutex_lock(&mtx);
        }
        void UnLockQueue()
        {
            pthread_mutex_unlock(&mtx);
        }
        void ProducterWait()
        {
            pthread_cond_wait(&_is_empty, &mtx);
        }
        void ConsumerWait()
        {
            pthread_cond_wait(&_is_full, &mtx);
        }
        void WakeUpProducter()
        {
            pthread_cond_signal(&_is_empty);
        }
        void WakeUpConsumer()
        {
            pthread_cond_signal(&_is_full);
        }
    private:
        std::queue<T> _bq;
        int _capacity; //队列容量上限
        pthread_mutex_t mtx;
        pthread_cond_t _is_full;
        pthread_cond_t _is_empty;
    };
}

我们需要知道,什么时候生产者线程开始等待,什么时侯消费线程开始等待。我们可以自己制定这个规则,此处我制定的规则为:当阻塞队列空了,消费者线程开始等待;当阻塞队列满了,生产者线程开始等待。
所以增加两个判断阻塞队列空满状态的函数

        bool IsFull()
        {
            return _bq.size() == _capacity;
        }
        bool IsEmpty()
        {
            return _bq.size() == 0;
        }

继续完善:

#pragma once
#include <queue>
#include <pthread.h>
//.hpp文件,可以声明和定义放在一起
//阻塞队列(交易场所)
namespace ns_blockqueue
{
    template <class T>
    class BlockQueue
    {
    public:
        BlockQueue(int cap = 5)
            :_capacity(cap)
        {
            pthread_mutex_init(&mtx, nullptr);
            pthread_cond_init(&_is_full, nullptr);
            pthread_cond_init(&_is_empty, nullptr);
        }
    public:
        void push(const T& data)
        {
            //生产函数
            LockQueue();
            if(IsFull())
            {
                //阻塞队列满了,生产者等待
                ProducterWait();
            }
            _bq.push(data);
            //当生产者生产成功后,消费者就可以消费了
            //唤醒在条件变量下的阻塞队列中等待的消费者线程
            WakeUpConsumer();
            UnLockQueue();
        }
        void pop(T* out)
        {
            //消费函数
            LockQueue();
            if(IsEmpty())
            {
                //阻塞队列空了,消费者等待
                ConsumerWait();
            }
            *out = _bq.front();
            _bq.pop();
            //消费者消费成功后,生产者就可以消费了
            //唤醒在条件变量下的阻塞队列中等待的生产者线程
            WakeUpProducter();
            UnLockQueue();
        }
        ~BlockQueue()
        {
            pthread_mutex_destroy(&mtx);
            pthread_cond_destroy(&_is_full);
            pthread_cond_destroy(&_is_empty);
        }
    private:
        void LockQueue()
        {
            pthread_mutex_lock(&mtx);
        }
        void UnLockQueue()
        {
            pthread_mutex_unlock(&mtx);
        }
        void ProducterWait()
        {
            pthread_cond_wait(&_is_empty, &mtx);
        }
        void ConsumerWait()
        {
            pthread_cond_wait(&_is_full, &mtx);
        }
        void WakeUpProducter()
        {
            pthread_cond_signal(&_is_empty);
        }
        void WakeUpConsumer()
        {
            pthread_cond_signal(&_is_full);
        }
        bool IsFull()
        {
            return _bq.size() == _capacity;
        }
        bool IsEmpty()
        {
            return _bq.size() == 0;
        }
    private:
        std::queue<T> _bq;
        int _capacity; //队列容量上限
        pthread_mutex_t mtx;
        pthread_cond_t _is_full;
        pthread_cond_t _is_empty;
    };
}

现在我们来测试一波。
修改消费者和生产者线程的行为时间。
消费者每间隔3秒后,取一次数据。生产者在不断地生产数据。

//消费者行为
void* consume(void* arg)
{
    BlockQueue<int> *bq = (BlockQueue<int>*)arg;
    //不断地从阻塞队列里拿取数据
    while(true)
    {
        int data = 0;
        sleep(3);
        bq->pop(&data);
        std::cout << "消费者线程拿到数据: " << data << std::endl;

    }
}
//生产者行为
void* product(void* arg)
{
    BlockQueue<int> *bq = (BlockQueue<int>*)arg;
    //不断制造数据,并放入阻塞队列
    while(true)
    {
        int data = rand() % 20 + 1;
        std::cout << "生产者线程成产数据" << data << ", 放入阻塞队列" << std::endl;
        bq->push(data);
        //sleep(2);
    }
}

运行的部分片段。
在这里插入图片描述
需要修一下的小细节。
BlockQueue类里的类成员函数pushpop函数里

if(IsFull())
{
	//阻塞队列满了,生产者等待
	ProducterWait();
}

if(IsFull())
{
	//阻塞队列满了,生产者等待
    ProducterWait();
}

如果线程等待失败了,它们还是会继续进行push和pop的操作,所以轮询地检测, 可以保证线程是因为条件满足而进行的push和pop的操作。

while(IsFull())
{
	//阻塞队列满了,生产者等待
	ProducterWait();
}

while(IsFull())
{
	//阻塞队列满了,生产者等待
    ProducterWait();
}

当然,上面所编写的内容,是让生产者线程往阻塞队列里放入数据,让消费者线程从阻塞队列里拿数据。这样做的意义并不大!让消费者线程从阻塞队列里面拿一个任务并完成这个任务才具有较大的意义。

为了做出这个测试,我定义了一个任务类。

class task
{
public:
    task(int x = 1, int y = 1, char op = '+')
        : _x(x), _y(y), _op(op)
    {
    }
    int run()
    {
        int ret;
        switch (_op)
        {
        case '+':
            ret = _x + _y;
            break;
        case '-':
            ret = _x - _y;
            break;
        case '*':
            ret = _x * _y;
            break;
        case '/':
            ret = _x / _y;
            break;
        case '%':
            ret = _x % _y;
            break;
        default:
            std::cout << "不支持该操作" << std::endl;
            break;
        }
        return ret;
    }

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

将CPer.cpp文件内容修改一下

class task
{
public:
    task(int x = 1, int y = 1, char op = '+')
        : _x(x), _y(y), _op(op)
    {
    }
    int run()
    {
        int ret;
        switch (_op)
        {
        case '+':
            ret = _x + _y;
            break;
        case '-':
            ret = _x - _y;
            break;
        case '*':
            ret = _x * _y;
            break;
        case '/':
            ret = _x / _y;
            break;
        case '%':
            ret = _x % _y;
            break;
        default:
            std::cout << "不支持该操作" << std::endl;
            break;
        }
        return ret;
    }

    int _x;
    int _y;
    char _op;
};

//消费者行为
void *consume(void *arg)
{
    BlockQueue<task> *bq = (BlockQueue<task> *)arg;
    //不断地从阻塞队列里拿取数据
    while (true)
    {
        task t;
        sleep(3);
        bq->pop(&t);
        std::cout << "消费者线程完成任务: " << t._x  << t._op <<t._y << "= "<< t.run() << std::endl;
    }
}
//生产者行为
void *product(void *arg)
{
    BlockQueue<task> *bq = (BlockQueue<task>*)arg;
    string str = "+-*/%";
    //不断制造数据,并放入阻塞队列
    while (true)
    {
        int x = rand() % 20 + 1;
        int y = rand() % 20 + 1;
        char op = rand() % 4 + 1;
        task t(x, y, str[op]);
        std::cout << "生产者线程派发任务: " << x << str[op] << y << "= ?"  << std::endl;
        bq->push(t);
        // sleep(2);
    }
}

int main(void)
{
    srand((long long)time(nullptr));
    //创建一个交易场所(阻塞队列)
    BlockQueue<task> *bq = new BlockQueue<task>;

    pthread_t c, p;
    pthread_create(&c, nullptr, consume, (void *)bq);
    pthread_create(&c, nullptr, product, (void *)bq);
    //主线程等待
    pthread_join(c, nullptr);
    pthread_join(p, nullptr);
    return 0;
}

在这里插入图片描述

POSIX信号量

信号量的本质是一把计数器,用来衡量临界资源中资源的数目。POSIX信号量可以用于线程间同步,达到无冲突地访问共享资源的目的。

以一个管理严格的图书管为例。
在这里插入图片描述
图书馆中资源是有限的,为了维持图书管的秩序,图书馆门口必定会有一个刷卡的门禁。每个人想要进去就需要提前预定和申请,申请成功了,里面必当会有一个属于自己的位置。如果申请不成功就说明里面此时资源全部被占用只能进行等待。

计算机里面也同样如此,临界资源被划分成一个一个的小资源,每个线程想要使用资源,都必须申请信号量,如果申请成功,一定会有自己可以占用的资源。

信号量的本质是一个计数器,用来衡量临界资源中资源的数目,那我们就直接给一个计数器。

int count = 5

如果一个线程申请成功一份资源了,那么count--;使用完后count++
但是count--count++的操作不是原子性的,多线程编程下线程不安全,所以它的伪代码如下。

在这里插入图片描述
申请信号量的操作可以称为P()操作;销毁信号量的操作可以称为V()操作。

POSIX信号量的接口
创建一个信号量
semt_t name


信号量的初始化

#include <semaphore.h>
int sem_init(sem_t *sem, int pshared, unsigned int value);

参数
sem:要初始化的信号量。
pshared:0表示该信号量线程间共享、非0表示进程间共享。
value:初始化信号量的值。


信号量的销毁

#include <semaphore.h>
int sem_destroy(sem_t *sem);

等待信号量

#include <semaphore.h>
int sem_wait(sem_t *sem);

作用:等待信号量,如果条件满足,会将信号量的值-1。对应着P()操作。


发布信号量

#include <semaphore.h>
int sem_post(sem_t *sem);

作用:发布信号量,表示资源使用完毕,可以归还资源,会将信号量的值+1。对应着V()操作。

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

基于阻塞队列的生产者-消费者模型,空间是可以动态分配的。还可以基于固定大小的环形队列,并利用信号量进行重新编写。
在这里插入图片描述
这里使用数组的方式来模拟环形队列。

环形队列的最大问题就是判断队列的空满,如果不加任何策略,那么队列为空和队列为满,队头和队尾指针指向是同一块空间,无法区分出队列为空还是为满。
判断队列空满状态的三种常见办法:

  • 制作一个计数器。
  • 设置布尔变量
  • 多开一个空间。

第三种方法是很常用的。这里选用第一种办法,信号量的本质就是一个计数器。

第一步,搭好框架。
所需文件

  • Makfile
  • CP.cpp
  • RingQueue.hpp

RingQueue.hpp文件

namespace ydy
{
    template<class T>
    class RingQueue
    {
    public:
        RingQueue(int cap = 5)
            :_capacity(cap)
        {}
        void push(const T& in)
        {
            //生产接口
        }
        void pop(T *out)
        {
            //消费接口
        }
    private:
        std::vector<T> _que;
        int _capcity;
    };
}

CP.cpp文件

#include <iostream>
#include <pthread.h>
#include "RingQueue.hpp"

void* consumer(void* args)
{
    //消费者行为
}
void* producter(void* args)
{
    //生产者行为

}
int main(void)
{
    ydy::RingQueue<int> *rq = new ydy::RingQueue<T>;

    pthread_t c, p;
    pthread_create(&c, nullptr, consumer, (void*)rq);
    pthread_create(&p, nullptr, producter, (void*)rq);

    pthread_join(c, nullptr);
    pthread_join(p, nullptr);
    return 0;
}

添加CP.cpp文件内的消费者和生产者的行为

#include <iostream>
#include <pthread.h>
#include <time.h>
#include <unistd.h>
#include "RingQueue.hpp"

void* consumer(void* args)
{
    //消费者行为
    ydy::RingQueue<int> *rq =(ydy::RingQueue<int>*)args;
    while(true)
    {
        int data = 0;
        rq->pop(&data)
        std::cout << "消费者拿数据:" << data << std:::endl;
    }
}
void producter(void* args)
{
    //生产者行为
    ydy::RingQueue<int> *rq =(ydy::RingQueue<int>*)args;
    while(true)
    {
        int data = rand() % 20 + 1;
        std::cout << "生产者放数据:" << data << std::endl;
        rq->push(data);
        sleep(2);
    }
}
int main(void)
{
    srand((long long)time(nullptr));
    ydy::RingQueue<int> *rq = new ydy::RingQueue<T>;

    pthread_t c, p;
    pthread_create(&c, nullptr, consumer, (void*)rq);
    pthread_create(&p, nullptr, producter, (void*)rq);

    pthread_join(c, nullptr);
    pthread_join(p, nullptr);
    return 0;
}

编写生产接口和消费接口
想要往环形队列里面放入数据,那么环形队列必须还有空的位置放入。对于生产者来说,它最关心的资源就是空位置。所以可以设置一个关于空位置资源的信号量,来衡量空位置的数目。消费者关心的是占有资源的位置,所以还可以设置一个关于有数据位置的信号量。

#include <semaphore.h>
namespace ydy
{
    template<class T>
    class RingQueue
    {
    public:
        RingQueue(int cap = 5)
            :_capacity(cap)
        {
            sem_init(&_EmptySpace, 0, cap);
            sem_init(&_DataSpace, 0, cap);
        }
        void push(const T& in)
        {
            //生产接口

            sem_wait(&_EmptySpace); //对应P()操作
            //往队尾空位置放入
            sem_post(&DataSpace); //对应V()操作

        }
        void pop(T *out)
        {
            //消费接口
            sem_wait(&_DataSpace); //对应P()操作
            //取出队头数据
            sem_post(&_EmptySpace); //对应V()操作
        }
        ~RingQueue()
        {
            sem_destroy(&_EmptySpace);
            sem_destroy(&_DataSpace);
        }
    private:
        std::vector<T> _que;
        int _capcity;
        sem_t _EmptySpace;
        sem_t _DataSpace;
    };
}

此时就犯难了,我们不清楚此时的队头和队尾分别在什么位置。这里可以设置两个成员变量来记录此时队头和队尾的位置。

#include <semaphore.h>
namespace ydy
{
    template<class T>
    class RingQueue
    {
    public:
        RingQueue(int cap = 5)
            :_capacity(cap)
        {
            sem_init(&_EmptySpace, 0, cap);
            sem_init(&_DataSpace, 0, cap);
            
            _rear = _tail = 0;
        }
        void push(const T& in)
        {
            //生产接口

            sem_wait(&_EmptySpace); //对应P()操作
            //往队尾空位置放入
            _que[_tail] = in;
            sem_post(&DataSpace); //对应V()操作

            _tail++;
            _tail %= _capacity; //维持环形队列
        }
        void pop(T *out)
        {
            //消费接口
            sem_wait(&_DataSpace); //对应P()操作
            //取出队头数据
            *out = _que[_rear];
            sem_post(&_EmptySpace); //对应V()操作

            _rear++;
            _rear %= _capacity; //维持环形队列
        }
        ~RingQueue()
        {
            sem_destroy(&_EmptySpace);
            sem_destroy(&_DataSpace);
        }
    private:
        std::vector<T> _que;
        int _capacity;
        sem_t _EmptySpace;
        sem_t _DataSpace;
        int _rear;
        int _tail;
    };
}

需要注意, std::vector _que;只是声明,像上面那样写,_que还没有被定义出来,所以运行会出现段错误。正确的RingQueue构造函数

        RingQueue(int cap = 5)
            :_que(cap)
            ,_capacity(cap)
        {
            sem_init(&_EmptySpace, 0, cap);
            sem_init(&_DataSpace, 0, cap);
            
            _rear = _tail = 0;
        }

生产接口和消费接口中,可能会有多个线程申请到信号量,为了保护临界资源,所以需要两个互斥量,一个用于消费者线程,一个用于生产者线程。
所以RingQueue.hpp的代码最终如下

namespace ydy
{
    template<class T>
    class RingQueue
    {
    public:
        RingQueue(int cap = 10)
            :_que(cap)
            ,_capacity(cap)
        {
            sem_init(&_EmptySpace, 0, cap);
            sem_init(&_DataSpace, 0, 0);
            
            _rear = _tail = 0;

            pthread_mutex_init(&_pmtx, nullptr);
            pthread_mutex_init(&_cmtx, nullptr);
        }

        void push(const T& in)
        {
            //生产接口

            sem_wait(&_EmptySpace); //对应P()操作
            pthread_mutex_lock(&_pmtx);
            //往队尾空位置放入
            _que[_tail] = in;
            sem_post(&_DataSpace); //对应V()操作

            _tail++;
            _tail %= _capacity; //维持环形队列
            pthread_mutex_unlock(&_pmtx);
        }
        void pop(T *out)
        {
            //消费接口
            sem_wait(&_DataSpace); //对应P()操作
            pthread_mutex_lock(&_cmtx);
            //取出队头数据
            *out = _que[_rear];
            sem_post(&_EmptySpace); //对应V()操作

            _rear++;
            _rear %= _capacity; //维持环形队列
            pthread_mutex_unlock(&_cmtx);
        }
        ~RingQueue()
        {
            sem_destroy(&_EmptySpace);
            sem_destroy(&_DataSpace);
            pthread_mutex_destroy(&_pmtx);
            pthread_mutex_destroy(&_cmtx);
        }
    private:
        std::vector<T> _que;
        int _capacity;
        sem_t _EmptySpace;
        sem_t _DataSpace;
        int _rear;
        int _tail;
        pthread_mutex_t _pmtx;
        pthread_mutex_t _cmtx;
    };
}

线程池

我们希望有线程去专门处理任务,这样可以提高效率。当一个任务产生的时候,去创建一个线程专门处理任务,然后这个线程再销毁。这样听起来似乎还不错,但我们可以提前准备好一些线程,用来随时处理产生的任务,将这些线程放在一个容器内,这个容器就称为线程池。

搭好基本框架
所需文件ThreadPool.hpp

namespace ydy
{
    template<class T>
    class ThreadPool
    {
    public:
        ThreadPool(int num = 5)
            :_que(num)
            ,_num(num)
        {
        }
        void ThreadPoolInit()
        {
            //初始化线程池
        }
        void PushTask(const T& in)
        {
            //往线程池内部的任务队列放任务
        }
        void PopTask(T* out)
        {
        }
    private:
        std::queue<T> _que; //任务队列
        int _num;
    };
}

先完成线程池的初始化,创建_num个线程,用来随时处理产生的任务。

        void* TaskProcess(void* arg)
        {
            
        }
        void ThreadPoolInit()
        {

            //初始化线程池
            pthread_t pt[_num];
            for(int i = 0; i < _num; i++)
            {
                pthread_create(&pt + i, nullptr, TaskProcess, (void*)" 待定传参");
            }
        }

TaskProcess函数在类中不能这样进行定义。因为线程执行函数的参数只能有一个void*的参数,但非静态的类成员方法会有一个隐含的this指针,所以这里不能把它定义成类内的静态成员方法。其中一种解决的办法就是把线程执行函数定义为静态的类成员方法。

static void* TaskProcess(void* arg)
{
}

任务处理函数,首先线程需要拿到任务队列里面的任务,并且线程池内所有线程需要看到的是同一个任务队列,因此任务处理函数需要拿到这个任务队列,所以创建线程时可以把任务队列传给线程执行函数。任务队列是ThreadPool类里的成员变量,传this指针就可以。

        static void* TaskProcess(void* arg)
        {
            //线程分离
            //不关心线程的返回值,线程退出时自动释放线程资源
            pthread_detach(pthread_self());

            ThreadPool<T>* tp = (ThreadPool<T>*)arg; //拿到任务队列

            //处理任务
        }
        void ThreadPoolInit()
        {

            //初始化线程池
            pthread_t pt[_num];
            for(int i = 0; i < _num; i++)
            {
                pthread_create(&pt + i, nullptr, TaskProcess, (void*)this);
            }
        }

任务队列对于线程池内的线程来说是临界资源,需要加锁来保护它的安全,这里引入互斥量。增加锁的创建、初始化、销毁、加锁、解锁

namespace ydy
{
    template<class T>
    class ThreadPool
    {
    public:
        ThreadPool(int num = 5)
            :_que(num)
            ,_num(num)
        {
            pthread_mutex_init(&_mtx, nullptr);
        }
        void lock()
        {
            pthread_mutex_lock(&_mtx);
        }
        void unlock()
        {
            pthread_mutex_unlock(&_mtx);
        }
        static void* TaskProcess(void* arg)
        {
            //线程分离
            //不关心线程的返回值,线程退出时自动释放线程资源
            pthread_detach(pthread_self());

            ThreadPool<T>* tp = (ThreadPool<T>*)arg; //拿到任务队列

            //处理任务
            while(true)
            {
                tp->lock();
                //
                tp->unlock();
            }
        }
        
        void ThreadPoolInit()
        {

            //初始化线程池
            pthread_t pt[_num];
            for(int i = 0; i < _num; i++)
            {
                pthread_create(&pt + i, nullptr, TaskProcess, (void*)this);
            }
        }
        void PushTask(const T& in)
        {
            lock();
            //往线程池内部的任务队列放任务
            unlock();
        }
        void PopTask(T* out)
        {
            lock();
            //
            unlock();
        }
        ~ThreadPool()
        {
            pthread_mutex_destroy(&_mtx);
        }
    private:
        std::queue<T> _que; //任务队列
        int _num;
        pthread_mutex_t _mtx;
    };
}

处理任务,线程需要判断队列里面是否为空,如果是空的,那么就需要等待,等待有任务放入任务队列,有任务之后就开始处理了。所以需要增加一个判断任务队列是否为空的函数。还需要增加一个等待函数,这个时候可以引入一个条件变量,让线程池内的线程都在这个关于该条件变量下的阻塞队列里面等待。增加判断任务队列是否为空的函数、互斥量的创建、初始化、销毁,等待、唤醒

namespace ydy
{
    template<class T>
    class ThreadPool
    {
    public:
        ThreadPool(int num = 5)
            :_que(num)
            ,_num(num)
        {
            pthread_mutex_init(&_mtx, nullptr);
            pthread_cond_init(&_cond, nullptr);
        }

        bool IsEmpty()
        {
            return _que.empty();
        }
        void wait()
        {
            pthread_cond_wait(&_cond, &_mtx);
        }
        void WakeUp()
        {
            pthread_cond_signal(&_cond);
        }
        void lock()
        {
            pthread_mutex_lock(&_mtx);
        }
        void unlock()
        {
            pthread_mutex_unlock(&_mtx);
        }
        static void* TaskProcess(void* arg)
        {
            //线程分离
            //不关心线程的返回值,线程退出时自动释放线程资源
            pthread_detach(pthread_self());

            ThreadPool<T>* tp = (ThreadPool<T>*)arg; //拿到任务队列

            //处理任务
            while(true)
            {
                tp->lock();
                while(tp->IsEmpty())
                {
                    //等待
                    tp->wait();
                }
                //有任务
                Task t;
                tp->PopTask(&t);
                tp->unlock();
                t.run();
            }
        }
        void ThreadPoolInit()
        {

            //初始化线程池
            pthread_t pt[_num];
            for(int i = 0; i < _num; i++)
            {
                pthread_create(&pt + i, nullptr, TaskProcess, (void*)this);
            }
        }
        void PushTask(const T& in)
        {
            lock();
            //往线程池内部的任务队列放任务
            unlock();
        }
        void PopTask(T* out)
        {
            lock();
            //
            unlock();
        }
        ~ThreadPool()
        {
            pthread_mutex_destroy(&_mtx);
            pthread_cond_destroy(&_cond)
        }
    private:
        std::queue<T> _que; //任务队列
        int _num;
        pthread_mutex_t _mtx;
        pthread_cond_t _cond;
    };
}

上面代码中TaskProcess函数内,先进行解锁了,再去解决任务,这样是比较好的,一个线程解决任务的时候,另一个线程就可以去等待/拿任务。

再来完成往任务队列里放任务和取任务的操作。往任务队列里放任务后,就可以唤醒等待的线程。

        void PushTask(const T& in)
        {
            lock();
            //往线程池内部的任务队列放任务
            _que.push_back(in);
            unlock();
            WakeUp();
        }
        void PopTask(T* out)
        {
            lock();
            *out = _que.front();
            _que.pop();
            unlock();
        }

所以这个简易的线程池最终完成代码:

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

namespace ydy
{
    template<class T>
    class ThreadPool
    {
    public:
        ThreadPool(int num = 5)
            :_que(num)
            ,_num(num)
        {
            pthread_mutex_init(&_mtx, nullptr);
            pthread_cond_init(&_cond, nullptr);
        }

        bool IsEmpty()
        {
            return _que.empty();
        }
        void wait()
        {
            pthread_cond_wait(&_cond, &_mtx);
        }
        void WakeUp()
        {
            pthread_cond_signal(&_cond);
        }
        void lock()
        {
            pthread_mutex_lock(&_mtx);
        }
        void unlock()
        {
            pthread_mutex_unlock(&_mtx);
        }
        static void* TaskProcess(void* arg)
        {
            //线程分离
            //不关心线程的返回值,线程退出时自动释放线程资源
            pthread_detach(pthread_self());

            ThreadPool<T>* tp = (ThreadPool<T>*)arg; //拿到任务队列

            //处理任务
            while(true)
            {
                tp->lock();
                while(tp->IsEmpty())
                {
                    //等待
                    tp->wait();
                }
                //有任务
                Task t;
                tp->PopTask(&t);
                tp->unlock();
                t.run();
            }
        }
        void ThreadPoolInit()
        {

            //初始化线程池
            pthread_t pt[_num];
            for(int i = 0; i < _num; i++)
            {
                pthread_create(&pt + i, nullptr, TaskProcess, (void*)this);
            }
        }
        void PushTask(const T& in)
        {
            lock();
            //往线程池内部的任务队列放任务
            _que.push_back(in);
            unlock();
            WakeUp();
        }
        void PopTask(T* out)
        {
            lock();
            *out = _que.front();
            _que.pop();
            unlock();
        }
        ~ThreadPool()
        {
            pthread_mutex_destroy(&_mtx);
            pthread_cond_destroy(&_cond)
        }
    private:
        std::queuer<T> _que; //任务队列
        int _num;
        pthread_mutex_t _mtx;
        pthread_cond_t _cond;
    };
}

需要注意在这里面我引入了Task类,Task类需要自己实现。

  • 3
    点赞
  • 24
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

小小酥诶

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值