目录
基本名词
1、临界资源:凡是被线程共享访问的资源都是临界资源(多线程、多进程打印数据到显示器[临界资源] )
2、临界区:代码中访问临界资源的代码(在我的代码中,不是所有的代码都是进行访问临界资源的。而访问临界资源的代码区域我们称之为临界区)
3、对临界区进行保护的功能,本质:就是对临界资源的保护。方式:互斥或者同步。
4、互斥:在任意时刻,只允许一个执行流访问某段代码(访问某部分资源),就可以称之为互斥!
5、原子性:比如:printf("hello bit") -> lock(); printf(); unlock(); -> 一个事情,要么不执行,要么就执行完毕
6、同步:一般而言,让访问临界资源的过程在安全的前提下(-般都是互斥and原子的),让访问资源具有一定的顺序性!具有合理性
观察临界资源
我们通过一个买票程序来理解以下临界资源在代码中的运用
tickets变量是个界资源,可以被多个线程同时看见,但是是否具有原子性呢?我们稍后再来分析。
int tickets = 1000; // 临界资源
void* pthread_run(void* args)
{
while (tickets > 0)
{
usleep(3000);
int i = *(int*)&args;
printf("我是[0x%x],正在购票……购票成功,当前剩余票 : %d\n", pthread_self(), tickets);
tickets--;
printf("");
}
}
int main()
{
pthread_t tid[5];
int i = 0;
while (i < 5)
{
int* id = new int(i);
pthread_create(&tid[i],NULL, pthread_run, (void*)id);
++i;
}
for (int i = 0; i < 5; ++i)
{
pthread_join(tid[i], NULL);
}
return 0;
}
先来分析一下这段代码:创建了5个线程,并且每个线程的函数是同一个,传进去的参数是id,也是主函数创建线程的序号,在线程里面实现的是购票,票数相应--,并且主函数在等待这几个线程的退出,当线程结束的时候,主线程也就退出了。这就是这段代码的简单叙述。那么接下来我们看一下结果
可以看见,我们购票居然购买到负数去了,这多出来的票不是该买的票,那么是如何导致的呢?
我们来看以下这张图
我们就明白了,其根本原因就是因为--操作不是原子性的,因为--操作是由3步骤完成,而不是一个步骤完成的,在步骤切换之间就可能存在线程被切换的可能性,而其他线程也在买票,该线程买完票回去,又买票,但此刻的票数不是最新的,所以就导致了票被多卖了。我们就可以知道了,这个--的过程是在pthread_run函数里面进行的,也就是说这一段是在访问临界资源,访问临界资源的代码段称为临界区,但是此时此刻的临界区存在着函数重入,会导致值的不正确,所以接下来我们着力于解决被重入,让他变得具有原子性。
到这里我么解释了该程序不具有原子性,以及为什么不具有原子性,接下来我们来看如何让其具有原子性。
线程上锁
那么我们应该如何解决这个问题呢?
// 上锁,在临界区使用前一刻进行上锁,让其他线程无法竞争该资源
int pthread_mutex_lock(pthread_mutex_t *mutex);
// 解锁,在临界区使用完的后一刻进行解锁让其他线程可以使用
int pthread_mutex_unlock(pthread_mutex_t *mutex);
// 初始化mutex互斥量
int pthread_mutex_init(pthread_mutex_t *restrict mutex,const pthread_mutexattr_t *restrict attr);
// 释放互斥量
int pthread_mutex_destroy(pthread_mutex_t *mutex);
// 初始化静态/全局互斥量,不用释放,会自动释放
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
class tickets
{
public:
tickets()
: _ticket(666)
{
//mtx = PTHREAD_MUTEX_INITIALIZER;
pthread_mutex_init(&mtx, NULL);
}
bool GetTicket()
{
pthread_mutex_lock(&mtx);
// 以下部分串行运行,不会存在资源竞争的问题
bool ret = true;
if (_ticket > 0)
{
usleep(500);
printf("我是[0x%x],正在购票……购票成功,当前剩余票 : %d\n", pthread_self(), _ticket);
_ticket--;
}
else
{
ret = false;
std::cout << "票已抢空" << std::endl;
}
pthread_mutex_unlock(&mtx);
return ret;
}
~tickets()
{
pthread_mutex_destroy(&mtx);
}
private:
int _ticket;
pthread_mutex_t mtx;
};
void* pthreadGetTicket(void* args)
{
tickets* t = (tickets*)args;
while(1)
{
if (!t->GetTicket())
{
break;
}
}
}
int main()
{
tickets* t = new tickets;
pthread_t tid[5];
int i = 0;
while (i < 5)
{
pthread_create(&tid[i],NULL, pthreadGetTicket, t);
++i;
}
for (int i = 0; i < 5; ++i)
{
pthread_join(tid[i], NULL);
}
return 0;
}
该代码段,主要通过一个C++的类来实现购票,每个线程通过ticket类的GetTicket函数来达到购票的目的,在该线程里面会不停的购票直到票被卖完,该函数通过返回一个布尔值,告诉线程当前是否还有票如果还有票就会继续买,但这段临界区是被上锁的,是处于串行运行的状态,就可以做到上面的三个步骤连续做而不会被其他线程重入这段临界区,那么在这里我们还会有疑惑,上锁是如何原子性的呢?接下来让我们来看以下这段代码和一张图:
我们如果想要自己实现一个锁,可以定义一个lock变量,当lock为1的时候表示还没有线程使用该临界区,反之则表示有线程在使用
int lock = 1
if (lock > 0) {
lock--;
}
else {
lock++;
}
但是这段代码行吗?依然会和之前面临一样的困境,在值进行--或者++操作的时候,会由于各种原因被其他线程调度,从而失去原子性。因此我们换一种思路
上锁是怎么个过程呢?将寄存器al的值置0,然后再与互斥锁进行值交换,此时该线程就已经上锁,mutex互斥量里面的锁也不在了,变成了0,如果时间片到了会被其他线程拿到锁吗?不会!因为在时间片用完的时候,发生了保护上下文的操作,会将寄存器的值存在线程的上下文中,此时锁就被带走到线程的上下文中了;如果此刻发生了其他线程想要申请的时候,会申请到吗?将他的al寄存器置0,再与mutex进行值交换,能行吗?此刻mutex的值就是0,无论如何都获取不了锁,也就无法进入临界区了。因此获得了锁的线程必定是要么不执行,要么就将临界区的代码执行完毕才会结束,因此有了原子性。那么在此期间也就不会存在其他线程来执行临界区代码了,也就不会导致票被多卖了。
线程拥有一个锁,该锁的声明周期可以概括如如下:
1、上锁,将锁抱走,同时给互斥量置0让其他线程抱不到锁
2、执行临界区代码,访问临界资源
3、时间片到了,将寄存器的值保存到.上下文,同时抱走锁,让其他线程不能被上锁;其他线程如果要申请互斥量是申请失败的,因为此时互斥量的值是0
4、再次调度线程,将上下文的内容重新加载到寄存器中,同时锁也加载进去了
5、执行完毕锁置1
这个过程是连续,不会中途执行其他代码,也不会被其他线程随意进入临界区。
线程的死锁
去申请一个永远申请不成功的资源就是死锁。
比如:
bool GetTicket()
{
pthread_mutex_lock(&mtx);
pthread_mutex_lock(&mtx);
// 以下部分串行运行,不会存在资源竞争的问题
bool ret = true;
if (_ticket > 0)
{
usleep(500);
printf("我是[0x%x],正在购票……购票成功,当前剩余票 : %d\n", pthread_self(), _ticket);
_ticket--;
}
else
{
ret = false;
std::cout << "票已抢空" << std::endl;
}
pthread_mutex_unlock(&mtx);
return ret;
}
连续申请了两次同一个锁,能成功吗?不能成功,因为第一次已经将锁申请成功了,al寄存器的值就位1了,此刻再去申请这个锁,又将al置0,再去与mutex进行交换,但是此刻两边的值都是0,因此就会一直卡在这里因为处于申请到了一个锁,然后线程又把这把锁丢了,想要重新去申请这把锁,但是这把锁已经找不到了,也就再也申请不到了,但又出不去,因此就卡着了。
总结
经过上面的例子,大家已经意识到单纯的i++或者++i都不是原子的,有可能会有数据一致性问题为了实现互斥锁操作,大多数体系结构都提供了swap或exchange指令,该指令的作用是把寄存器和内存单元的数据相交换,由于只有一条指令,保证了原子性,即使是多处理器平台,访问内存的总线周期也有先后,一个处理器上的交换指令执行时另一个处理器的交换指令只能等待总线周期。
线程安全与函数重入
线程安全:多个线程并发同一段代码时,不会出现不同的结果。常见对全局变量或者静态变量进行操作,并且没有锁保护的情况下,会出现该问题。 重入:同一个函数被不同的执行流调用,当前一个流程还没有执行完,就有其他的执行流再次进入,我们称之为重入。一个函数在重入的情况下,运行结果不会出现任何不同或者任何问题,则该函数被称为可重入函数,否则,是不可重入函数。
常见的线程不安全
1、不保护共享变量的函数,比如第一个例子的tickets
2、函数状态随着被调用,状态发生变化的函数,比如第一个例子的--操作
3、返回指向静态变量指针的函数
4、调用线程不安全函数的函数,比如第一个例子的pthread_run函数
常见的线程安全情况
1、每个线程对全局变量或者静态变量只有读取的权限,而没有写入的权限,-般来说这些线程是安全的
2、类或者接口对于线程来说都是原子操作
3、多个线程之间的切换不会导致该接口的执行结果存在二义性
常见不可重入的情况
1、调用了mall/free函数, 因为malloc函数是用全局链表来管理堆的
2、调用了标准IO库函数,标准I/O库的很多实现都以不可重入的方式使用全局数据结构
3、可重入函数体内使用了静态的数据结构
常见的可重入情况
1、不使用全局变量或静态变量
2、不使用用malloc或者new开辟出的空间
3、不调用不可重入函数
4、不返回静态或全局数据,所有数据都有函数的调用者提供
5、使用本地数据,或者通过制作全局数据的本地拷贝来保护全局数据
可重入与线程安全联系
1、函数是可重入的,那就是线程安全的
2、函数是不可重入的,那就不能由多个线程使用,有可能引发线程安全问题
3、如果一个函数中有全局变量,那么这个函数既不是线程安全也不是可重入的。
可重入与线程安全区别
1、可重入函数是线程安全函数的一种
2、线程安全不一定是可重入的,而可重入函数则一定是线程安全的。比如之前的死锁例子。
3、如果将对临界资源的访问加上锁,则这个函数是线程安全的,但如果这个重入函数若锁还未释放则会产生死锁,因此是不可重入的。
死锁四个必要条件
互斥条件:一个资源每次只能被一 一个执行流使用
请求与保持条件:一个执行流因请求资源而阻塞时,对已获得的资源保持不放
不剥夺条件:一个执行流已获得的资源,在末使用完之前,不能强行剥夺
循环等待条件:若干执行流之间形成一 种头尾相接的循环等待资源的关系
避免死锁
1、破坏死锁的四个必要条件
2、加锁顺序一致
3、避免锁未释放的场景
4、资源一次性分配
同步
同步:一般而言,让访问临界资源的过程在安全的前提下(一般都是互斥and原子的),让访问资源具有一定的顺序性!具有合理性
条件变量
但是通过锁来竞争资源,我们不能知道当前的临界资源处于怎么样的状态。
举个栗子:你想要去面包店买面包,你就要进去不断的询问店家面包做好了没有,店家告诉你没有做好,一分钟后,你又去问面包做好了没有,得到了同样的答复,如此往复,总是你在问,此期间是有大量的资源浪费出现的,因此这种锁竞争的策略在这里似乎显得不怎么好用了,我们不能轻松的知道目前临界资源的状态是否可以支持我们去成功申请资源了,而是需要不断的循环访问,为了解决这种没必要的浪费,于是就有了条件变量。我们可以去跟店家说,面包做好了的时候叫我一下啊,当面包做好了,就来通知你购买,而期间就没有了循环访问的浪费资源的情况。这就是条件变量,当满足预先的条件就会自动转到运行队列,从而申请到锁。
函数原型
int pthread_cond_init(pthread_cond_t *restrict cond,
const pthread_condattr_t *restrict attr);
// cond,对条件变量进行初始化
// attr,一般设置为你nullptr即可
int pthread_cond_destroy(pthread_cond_t *cond);
// 释放条件变量
pthread_cond_t cond = PTHREAD_COND_INITIALIZER;
// 如果条件变量是全局或者是静态的,就可以以这种形式来初始化
int pthread_cond_wait(pthread_cond_t *restrict cond,
pthread_mutex_t *restrict mutex);
// 功能 : 让锁等待条件变量cond
// cond : 表示该线程在收到cond信号的时候,就会配合其他调度函数转到运行队列,当申请到锁,就开始以原子性的执行
// mutex : 表示一进入这个函数,就会自动将锁释放掉,无需手动释放锁
int pthread_cond_broadcast(pthread_cond_t *cond);
// 唤醒依赖该条件变量的全部线程转入到运行队列
int pthread_cond_signal(pthread_cond_t *cond);
// 唤醒依赖该条件变量的一个线程转入到运行队列
实例
#include <iostream>
#include <string>
#include <pthread.h>
#include <unistd.h>
using namespace std;
pthread_mutex_t mtx;
pthread_cond_t cond;
void* Boss(void* args)
{
string boss = *(string*)args;
while(true)
{
// pthread_cond_broadcast(&cond);
pthread_cond_signal(&cond);
cout << "派发任务:" << boss << endl;
sleep(1);
}
}
void* Worker(void* args)
{
string worker = *(string*)args;
while(true)
{
pthread_cond_wait(&cond, &mtx);
cout << "当前执行者:" << pthread_self() << "执行任务:" << worker << endl;
}
}
int main()
{
pthread_t worker[4], boss;
pthread_mutex_init(&mtx, nullptr);
pthread_cond_init(&cond, nullptr);
string strBoss = "boss";
string strWorker = "worker";
pthread_create(&boss, nullptr, Boss, (void*)&strBoss);
for (int i = 0; i < 4; ++i)
{
pthread_create(worker + i, nullptr, Worker, (void*)&strWorker);
}
pthread_join(boss, nullptr);
for (int i = 0; i < 4; ++i)
{
pthread_join(worker[i], nullptr);
}
pthread_mutex_destroy(&mtx);
pthread_cond_destroy(&cond);
return 0;
}
这个程序实现的就是boss派发任务与worker执行任务。有两类线程,boss线程与worker线程,worker线程是对该条件变量进行等待,当boss线程执行,就会发生信号,当worker收到信号就会开始执行。
两种信号发送方式我们都试一下就可以明显的看出区别了。
生产者消费者模型
而在之前小明进入自习室这个例子,可能会出现,小明刚刚自习完出来了,因为他离锁最近,又把锁申请到了,这样反复,就会导致其他人申请不到资源,从而产生饥饿问题,而为了有效的避免这种情况的发生,我们接下来便引入了同步的机制,规定释放完锁之后,必须要去排队才能重新申请锁,从而保证所有人访问这件自习室(临界资源)是按照一定的顺序执行的。
先从代码的角度来理解生产者消费者模型:
以上是两段代码,如果函数A要调用函数B,他们是串行执行的,只有当函数B执行完了,才能回来继续执行函数A,这两个函数之间交互的本质是什么?是数据通信。再把函数替换成线程
这两个线程通过临界资源来进行通信,简单的理解就是线程A将数据放入临界资源,线程B再从其中将数据取出来,这么一趟串行的执行。但是他们有可能并发执行吗?有可能
接下来我们从生活中找一个实例,超市!假设超市只卖面包。
首先在这里面出现了3类角色,生产者,消费者。
我们先来思考一下为什么是要有超市这个中间缓冲区呢?而不是直接让消费者去生产者那里直接购买面包呢?如果我们去生产者那里去购买,是不是会太麻烦了呢?也就是说交易的成本太高了,而超市的作用就是收集各种用户的需求,消费者需要购买商品,那么超市就去将商品统筹起来,而生产者担心商品卖不出去,便将商品卖给超市这种需要大量商品的地方,于是消费者就可以直接在超市选购商品,生产者直接一次性大量卖商品给超市,形成一个交易环,同时也减少了各方交易成本,同时生产者的生成不需要消费者的参与,消费者的消费也不需要直接去访问生产者,因此达到了解耦,也因为消费的方式变得简单了,也就提高了效率。
接下来我们思考生产者与生产者,消费者与消费者,生产者与消费者之间
生产者与生产者:竞争,互斥,生产者之间不能同时访问超市的同一片资源,是需要竞争的
消费者与消费者:消费者也是如此
生产者与消费者:互斥的,假设超市就只能摆放一个面包,此时消费者要消费这个面包,但是生产者想要在此区域放一个面包,于是双方就在互斥的访问该区域。
但是我们消费者是如何知道可以去消费了呢?就像之前的worker与boss的例子,worker是如何知道可以开始工作了呢?是不是worker与cond条件变量进行绑定,当收到这个信号就开始工作了啊?那这个变量是由谁来发出的呢?是不是boss呢?是的!所以只有boss知道worker什么时候该工作。同理,生产者什么时候知道自己该生产了呢?是不是当商品没有的时候就该生产了呢?商品没有了应该由谁来检测呢?是消费者还是生产者呢?这里大家可能会产生疑惑,如果是生产者来检测,那每一次制造的时候都要来检测一次是否有物品,这种做法合理不啊?不合理啊,生产者只需要检测是否生成满了就可以了不需要检测是否为空,所以,商品为空是由消费者来检测的,换句话说也就是只有消费者才能知道生产者什么时候该生产,只有生产者知道消费者什么时候该消费。到这里消费者生产者模型就达成闭环。接下来开始编写一段代码实现以下功能:
BQ.hpp头文件
#pragma once
#include <iostream>
#include <cstdio>
#include <stdlib.h>
#include <unistd.h>
#include <queue>
using namespace std;
namespace HB
{
#define DEFAULT_NUM 5
template <class T>
class BlockQueue
{
private:
std::queue<T> _bq; // 阻塞队列作为缓冲区、临界资源
int _cap; // 队列容量限制,当满了就停止生产
int _count = 0; // 返回生产者制造的数量
int _sell = 0; // 返回消费者购买的数量
pthread_mutex_t _mtx; // 互斥锁
pthread_cond_t _is_full; // 当满了,就停止生产,消费者在该条件下进行等待
pthread_cond_t _is_empty; // 当空了,就停止消费
void LockQueue() //在生产者或者消费者进行中,对其进行互斥访问
{
pthread_mutex_lock(&_mtx);
}
void UnLockQueue()
{
pthread_mutex_unlock(&_mtx);
}
bool ISFull()
{
return _bq.size() == _cap;
}
bool ISEmpty()
{
return _bq.size() == 0;
}
// 在调用pthread_cond_wait函数的时候,会自动释放_mtx锁从而挂起,当条件变量满足的时候,会自动申请获取锁
void ProductorWait() // 生产者等待
{
pthread_cond_wait(&_is_empty, &_mtx);
}
void ConsumerWait() // 消费者等待
{
pthread_cond_wait(&_is_full, &_mtx);
}
void WakeupProductor() // 唤醒生产者
{
pthread_cond_signal(&_is_empty);
}
void WakeupConsumer() // 唤醒消费者
{
pthread_cond_signal(&_is_full);
}
public:
BlockQueue(int cap = DEFAULT_NUM)
:_cap(DEFAULT_NUM)
{
pthread_mutex_init(&_mtx, nullptr);
pthread_cond_init(&_is_full, nullptr);
pthread_cond_init(&_is_empty, nullptr);
}
~BlockQueue()
{
pthread_mutex_destroy(&_mtx);
pthread_cond_destroy(&_is_full);
pthread_cond_destroy(&_is_empty);
}
int RetCount()
{
return _count;
}
int RetSell()
{
return _sell;
}
// 生产者生产
// 1、需要对临界资源访问,因此必须要是互斥的,因此上锁
// 2、如果已经生产满了,就停止生产,并且是个询查条件
// 3、没有满就生产物品进去
// 4、如果生产的物品有上限的一半了就唤醒消费者,告诉消费者你可以来购买商品了
// 5、对临界资源的访问已经结束,释放锁
void push(const T& in)
{
LockQueue();
while (ISFull())
{
ProductorWait();
}
this->_count++;
_bq.push(in);
if (_bq.size() > _cap / 2)
{
WakeupConsumer();
}
UnLockQueue();
}
void pop(T* out)
{
LockQueue();
while (ISEmpty())
{
ConsumerWait();
}
this->_sell++;
*out = _bq.front();
_bq.pop();
if (_bq.size() < _cap / 2)
{
WakeupProductor();
}
UnLockQueue();
}
};
}
task.hpp
#pragma once
#include <iostream>
#include <string>
namespace task
{
class algorithm
{
public:
int _x;
int _y;
char _flag;
public:
algorithm(int x = 1, int y = 1, char flag = '+')
:_x(x)
,_y(y)
,_flag(flag)
{}
~algorithm()
{}
public:
int Ret()
{
switch (_flag)
{
case '+':
return _x + _y;
break;
case '-':
return _x - _y;
break;
case '*':
return _x * _y;
break;
case '/':
return _x / _y;
break;
default:
break;
}
}
string str1()
{
string message = to_string(_x);
message += _flag;
message += to_string(_y);
message += "=?";
return message;
}
string str2()
{
string message = to_string(_x);
message += _flag;
message += to_string(_y);
message += "=";
return message;
}
};
}
con_pro.cc
#include "BQ.hpp"
#include "task.hpp"
#include <time.h>
using namespace std;
using namespace task;
using namespace BQ;
void* Producter(void* args)
{
BlockQueue<algorithm>* bq = (BlockQueue<algorithm>*)args;
while(true)
{
algorithm t;
t._x = rand() % 20 + 1;
t._y = rand() % 20 + 1;
t._flag = '+';
bq->push(t);
cout << "生产了数据 : " << t.str1() << endl;
usleep(50000);
}
}
void* Consumer(void* args)
{
BlockQueue<algorithm>* bq = (BlockQueue<algorithm>*)args;
while(true)
{
algorithm x;
bq->pop(&x);
cout << "购买到数据 : " << x.str2() <<x.Ret() << endl;
}
}
int main()
{
srand((long long)time(nullptr));
BlockQueue<algorithm>* bq = new BlockQueue<algorithm>;
pthread_t pro, con;
pthread_create(&pro, nullptr, Producter, (void*)bq);
pthread_create(&con, nullptr, Consumer, (void*)bq);
pthread_join(pro, nullptr);
pthread_join(con, nullptr);
return 0;
}
其运行过程可以参考如下:
信号量
信号量:是一把计数器,描述临界资源数目的大小,计数器被合理使用,可以达到对临界资源预定的目的。
举个栗子:我们去电影院买票,是不是只要买到这张票了,那么对应的位置就是你的了,不会有人去抢这个位置。但是票数是有限制的,只有那么多张票,卖完就没了,因此需要一个变量来进行计数,但是普通的count的 ++ 或者 -- 操作不是原子性的,因此在之前的使用都是需要对其进行上锁的,接下来当我们学习了信号量之后,就不用对计数器上锁了,因为信号量的PV操作都是原子的。
函数原型
// 信号量初始化
// sem : 对应的信号量
// pshared : 对线程进行信号量初始化就设置0,还有一种进程间的信号量
// value : 信号量的初始值
int sem_init(sem_t *sem, int pshared, unsigned int value);
// 释放信号量
int sem_destroy(sem_t *sem);
// P操作,申请某个资源,但是不是真的申请而是以执行--操作,来充当申请资源成功
int sem_wait(sem_t *sem);
// V操作,释放某个资源,但是不是真的释放,而是以执行++操作,来充当释放资源成功
int sem_post(sem_t *sem);
生产者消费者模型更改版
主体思路与之前的没有变化,依然是生产者与消费者两类主体。不过在此基础上,我们要使用环形队列,不使用条件变量而是使用信号量,同时加入两个互斥锁。加锁的原因是同类型的主体是需要互斥的,并且达到不同主体的并发的目的。在生产者这里,生产者看到的位置而不是数据,消费者看到的是数据而不是位置,所以他们互相对应的信号量一个是位置是否还有,一个数数据是否有。同时,相同实体之间是要进行互斥访问的,所以在信号量申请到了之后,就要进行加锁保证临界区的原子性。
ring_queue.hpp
#pragma once
#include <iostream>
#include <pthread.h>
#include <vector>
#include <semaphore.h>
#include <unistd.h>
namespace RingQueue
{
const int cap_default = 10;
template <class T>
class ringQueue
{
private:
std::vector<T> _rq; // 用于消费者与生产者互相通信的信道
int _cap; // 信道最大容量
sem_t _blank_sem; // 生产者看到的资源是空间,生产者的信号量
sem_t _data_sem; // 消费者看到的资源是数据,消费者的信号量
int _p_index; // 用于表示信道的下标
int _c_index;
pthread_mutex_t _p_mtx; // 用于生产者之间互斥访问
pthread_mutex_t _c_mtx; // 用于消费者之间互斥访问
public:
ringQueue(int cap = cap_default)
:_rq(cap)
,_cap(cap)
{
sem_init(&_blank_sem, 0, _cap);
sem_init(&_data_sem, 0, 0);
_p_index = _c_index = 0;
pthread_mutex_init(&_p_mtx, nullptr);
pthread_mutex_init(&_c_mtx, nullptr);
}
~ringQueue()
{
sem_destroy(&_blank_sem);
sem_destroy(&_data_sem);
pthread_mutex_destroy(&_p_mtx);
pthread_mutex_destroy(&_c_mtx);
}
public:
void push(const T& in)
{
// 需要将申请信号量的操作提前,因为对于生产者来说,
// 不是在获取到了锁才能申请信号量的,而是预先申请信号量,
// 当锁到了的时候,就能直接在对应的位置进行操作
// 从而提高与消费者之间并发性
// P操作是原子操作
sem_wait(&_blank_sem);
// 要保证中间的操作是原子操作因此要上锁
pthread_mutex_lock(&_p_mtx);
_rq[_p_index] = in;
_p_index++;
_p_index %= _cap;
// 当操作完毕就解锁,让其他生产者来获取锁
pthread_mutex_unlock(&_p_mtx);
// V操作也是原子操作
sem_post(&_data_sem);
}
void pop(T* out)
{
sem_wait(&_data_sem);
pthread_mutex_lock(&_p_mtx);
*out = _rq[_c_index];
_c_index++;
_c_index %= _cap;
pthread_mutex_unlock(&_p_mtx);
sem_post(&_blank_sem);
}
};
}
ring_queue.cc
#include "ring_queue.hpp"
#include "task.hpp"
#include <time.h>
using namespace RingQueue;
using namespace task;
char str[] = "+-*/";
void* consumer(void* args)
{
ringQueue<algorithm>* rq = (ringQueue<algorithm>*)args;
while(true)
{
algorithm t;
rq->pop(&t);
cout << "获得数据:" << t.str2() << t.Ret() << endl;
sleep(1);
}
}
void* producter(void* args)
{
ringQueue<algorithm>* rq = (ringQueue<algorithm>*)args;
while(true)
{
algorithm t;
t._x = rand() % 30;
t._y = rand() % 30;
t._flag = str[rand() % sizeof(str)];
rq->push(t);
//t._flag = '+';
cout << "生产数据:" << t.str1() << endl;
sleep(1);
}
}
int main()
{
srand((long long)time(nullptr));
ringQueue<algorithm>* rq = new ringQueue<algorithm>;
pthread_t con0, con1, con2, con3, pro0, pro1, pro2, pro3;
pthread_create(&con0, nullptr, consumer, (void*)rq);
pthread_create(&con1, nullptr, consumer, (void*)rq);
pthread_create(&con2, nullptr, consumer, (void*)rq);
pthread_create(&con3, nullptr, consumer, (void*)rq);
pthread_create(&pro0, nullptr, producter, (void*)rq);
pthread_create(&pro1, nullptr, producter, (void*)rq);
pthread_create(&pro2, nullptr, producter, (void*)rq);
pthread_create(&pro3, nullptr, producter, (void*)rq);
pthread_join(con0, nullptr);
pthread_join(con1, nullptr);
pthread_join(con2, nullptr);
pthread_join(con3, nullptr);
pthread_join(pro0, nullptr);
pthread_join(pro1, nullptr);
pthread_join(pro2, nullptr);
pthread_join(pro3, nullptr);
return 0;
}