鸡蛋工厂
多线程的生产和消费
main.cpp
#include <iostream>
#include <queue>
#include <mutex>
#include <atomic>
#include <condition_variable>
#include <chrono>
#include <thread>
#include <functional>
#include "mclog.h"
#include "rand_int.h"
#include "replace_fix.h"
// 对容器线程安全的简单封装
template <typename T>
class queue_th
{
public:
void push(T &&val)
{
// 上锁
std::lock_guard<std::mutex> lock(_mut);
// 完美转发数据,在中间层转发常用方式
_que.push(std::forward<T>(val));
}
std::pair<bool, T> pop_safe()
{
// 取判空和数据需要一气呵成,否则没有互斥的情况下会出现多次弹出的问题
std::lock_guard<std::mutex> lock(_mut);
if (_que.empty() == false)
{
T ret = _que.front();
_que.pop();
return std::make_pair(true, ret);
}
return std::make_pair(false, T());
}
size_t size()
{
return _que.size();
}
bool is_empty()
{
return _que.empty();
}
private:
// STL的容器是线程不安全的,在多线程使用需要给容器上锁
std::mutex _mut;
std::queue<T> _que;
};
// 鸡蛋组信息
struct info_eggs
{
int tag = 0;
int size = 0;
size_t date = 0;
std::vector<int> id;
};
// 鸡蛋工厂
class factory_eggs
{
public:
factory_eggs() : _run(true)
{
// 销毁函数
auto fn_delete = [](std::thread *th)
{
th->join();
MCLOG("线程退出")
};
// 三个发货工人
auto fn_package = std::bind(&factory_eggs::work_package_eggs, this);
auto sp_package_1 = std::shared_ptr<std::thread>(new std::thread(fn_package), fn_delete);
auto sp_package_2 = std::shared_ptr<std::thread>(new std::thread(fn_package), fn_delete);
auto sp_package_3 = std::shared_ptr<std::thread>(new std::thread(fn_package), fn_delete);
// 一个采集机器
auto fn_collect = std::bind(&factory_eggs::work_collect_eggs, this);
auto sp_collect_1 = std::shared_ptr<std::thread>(new std::thread(fn_collect), fn_delete);
auto sp_collect_2 = std::shared_ptr<std::thread>(new std::thread(fn_collect), fn_delete);
_works.push_back(sp_package_1);
_works.push_back(sp_package_2);
_works.push_back(sp_package_3);
_works.push_back(sp_collect_1);
_works.push_back(sp_collect_2);
}
~factory_eggs()
{
// 关闭运行,线程准备退出
_run = false;
// 通知所有线程启动,准备退出
_cond.notify_all();
MCLOG("退出鸡蛋工厂 " $(_que.size()));
}
// 注册发货信息接收
void register_transport(std::function<void(info_eggs)> fn)
{
_fn_transport = fn;
}
private:
// 人工打包并发货
void work_package_eggs()
{
MCLOG("进入发货流程");
while (_run || _que.size() > 0)
{
info_eggs ct;
// 进入上锁区域
{
// 使用 while 而不是 if 判断,防止空唤醒,如果唤醒时是空可以自己休眠
std::unique_lock<std::mutex> lock(_mut);
while (_que.is_empty() && _run)
{
// 如果数据为空,线程休眠,等待鸡蛋收集
_cond.wait(lock);
}
_tag++;
ct.tag = _tag;
ct.size = 6;
ct.date = std::chrono::steady_clock::now().time_since_epoch().count();
for (int i = 0; i < ct.size; i++)
{
// 判断是否能接收满一打,接收不到就等
while (_que.is_empty() && _run)
{
_cond.wait(lock);
}
// 上一行 is_empty 不为空,进入到这一行之后,多线程下还是存在为空的可能
// 取数据需要判空和弹出一气呵成,否则不安全
auto pair = _que.pop_safe();
if (pair.first)
{
ct.id.push_back(pair.second);
}
}
// 到这会退出作用域,解锁
}
// 无锁区域,发货
if (_fn_transport)
{
_fn_transport(ct);
}
// 人工发货,发货几次后休息
static int count = 0;
count++;
if (count > 3)
{
count = 0;
relax_time();
}
}
MCLOG("退出发货流程 " $(_que.size()));
}
// 机器采集,全年无休
void work_collect_eggs()
{
MCLOG("进入采集流程");
while (_run)
{
if (_que.size() < 5000)
{
rand_int rand_size(1, 36);
rand_int rand_id(0, 10000);
for (int i = 0; i < rand_size.value(); i++)
{
// 存放数据
_que.push(rand_id.value());
}
// 通知一个线程启动
_cond.notify_one();
}
}
MCLOG("退出采集流程 " $(_que.size()));
}
// 采集之后的休息时间
void relax_time()
{
// 静态变量,一个程序只声明和定义一次,直到程序总结才会销毁
static rand_int rand_ms(10, 50);
std::this_thread::sleep_for(std::chrono::milliseconds(rand_ms.value()));
}
private:
// 分发鸡蛋时使用
int _tag = 0;
std::function<void(info_eggs)> _fn_transport = nullptr;
// 控制线程数据交换时使用
std::atomic<bool> _run;
queue_th<int> _que;
std::mutex _mut;
// 条件变量,在多线程下,用于唤醒休眠的线程
// 线程可以通过 condition_variable 控制休眠和唤醒,用于在多线程下同步数据
std::condition_variable _cond;
// 管理智能指针的声明周期
std::vector<std::shared_ptr<std::thread>> _works;
};
int main(int argc, char **argv)
{
{
// 鸡蛋工厂
factory_eggs factory;
// 打印发货后鸡蛋信息
factory.register_transport([](info_eggs ct) {
// 这里是回调函数,在鸡蛋工厂的子线程内执行
std::string str = "tag: {} size: {} data: {}\n{}";
std::string str_id;
for (int i = 0; i < ct.id.size(); i++)
{
str_id += "#" + std::to_string(ct.id[i]) + " ";
}
str = replace_fix(str)(ct.tag, ct.size, ct.date, str_id);
// MCLOG($(str));
});
// 主线程挂机等待,3秒后自动关闭
MCLOG("等待工厂关闭")
std::this_thread::sleep_for(std::chrono::seconds(3));
}
MCLOG("退出程序")
return 0;
}
打印结果
进入发货流程 [/home/red/open/github/mcpp/example/20/main.cpp:114]
进入发货流程 [/home/red/open/github/mcpp/example/20/main.cpp:114]
进入发货流程 [/home/red/open/github/mcpp/example/20/main.cpp:114]
进入采集流程 [/home/red/open/github/mcpp/example/20/main.cpp:171]
等待工厂关闭 [/home/red/open/github/mcpp/example/20/main.cpp:233]
进入采集流程 [/home/red/open/github/mcpp/example/20/main.cpp:171]
退出采集流程 [_que.size(): 退出采集流程 [_que.size(): 退出鸡蛋工厂 [_que.size(): 50175017] 5017] [/home/red/open/github/mcpp/example/20/main.cpp [] :/home/red/open/github/mcpp/example/20/main.cpp185] [:
/home/red/open/github/mcpp/example/20/main.cpp:185]
101]
退出发货流程 [_que.size(): 0] [/home/red/open/github/mcpp/example/20/main.cpp:165]
线程退出 [/home/red/open/github/mcpp/example/20/main.cpp:76]
退出发货流程 [_que.size(): 0] [/home/red/open/github/mcpp/example/20/main.cpp:165]
退出发货流程 [_que.size(): 0] [/home/red/open/github/mcpp/example/20/main.cpp:165]
线程退出 [/home/red/open/github/mcpp/example/20/main.cpp:76]
线程退出 [/home/red/open/github/mcpp/example/20/main.cpp:76]
线程退出 [/home/red/open/github/mcpp/example/20/main.cpp:76]
线程退出 [/home/red/open/github/mcpp/example/20/main.cpp:76]
退出程序 [/home/red/open/github/mcpp/example/20/main.cpp:237]
多线程的生产和消费者模型,一个多线程中使用最多的例子,而这个例子也是实际应用中会用到的,它模拟了很多的数据输入,然后让你去处理这些输入的数据,处理完成的数据转为输出
当然在这个模型中,输入和输出都可以是多线程的,或者就算他们都是单线程也最低需要两个线程才能满足输入输出
本篇文章使用一个鸡蛋工厂来模拟这个经典的多线程生产消费模型,完成这个模型你需要了解到的是使用不同的线程分别执行生产方和消费方的工作函数,消费方会因为没有任务或者其他原因休息,生产方总是要提醒消费方进入工作状态,他们之间需要通过管道以排队的方式传递数据,同一时间下数据的存放和获取只能二选一
了解这些运行方式之后,接下里具体分析一下鸡蛋工厂的工作原理
工作原理
在 main.cpp 文件中的 factory_eggs 鸡蛋工作中可以看到,创建对象后在构造函数中直接运行了5个线程,其中3个线程用于打包发货,2个线程用于采集鸡蛋,这些线程由智能智能管理声明周期,线程的生命周期最后会和 factory_eggs 同步销毁
生产方,也就是采集鸡蛋的线程会运行 work_collect_eggs 函数,这个函数负责收集鸡蛋,每次收集的鸡蛋个数都是随机的,收集后存入到数据管道中,并通知消费方进行处理
消费方,负责打包发货的线程会运行 work_package_eggs 函数,这个函数会从数据管道中获取到每一颗的鸡蛋,并将每6个鸡蛋作为一打进行打包,如果数据管道为空,则会进入休眠等待鸡蛋收集和生产方的下一个通知,注意这里的每一颗鸡蛋都有ID,能确保收集的和打包的是同一个鸡蛋
值得注意的是,如果你的消费方线程进入休眠了,却没有生产方的线程通知唤醒,就会进入到一种生产方不停生产却没有线程进行消费的情况,这会导致程序执行逻辑错误
你会看到这个鸡蛋工厂中,鸡蛋收集方会在鸡蛋数量大于某个值之后不收集了,发货工人也时不时偷懒,这是一种限流模拟
作为收集方,当生产的进度太快就会吃掉太多性能,让消费方无法快速处理,你需要平衡他们之间的数据差以限制接收部分生产方的内容,转为消费方,速度比生产方慢几乎是必然的,你需要火力全开的同时还尽可能的优化处理速度
线程容器
你会看到 queue_th 容器简单的封装了 std::queue 这个STL标准库的队列容器,在多线程下,你的共享数据是可能被损坏的,所以一个数据被多个线程同时操作时你需要上锁
在C++中没有默认的线程安全的容器可以使用,你需要对容器进行简单的封装,但由于容器本身不是多线程版本,封装之后效率会有所下降,如果你需要更高的效率通常需要使用第三方库
不过这种简单的封装性能依旧够快,如果你不想使用第三方库的同时需要更快一点的性能,你可以使用自旋锁来封装容器可以稍微提升一点读写速度
自旋锁是一种利用CPU空转的方式等待解锁,对于进入休眠来说通常可以更快的获取到锁并继续执行
休眠和唤醒条件
消费方如果在空闲时会进入休眠状态,需要工作时会被唤醒,线程的休眠和唤醒需要 condition_variable 条件变量进行控制,它可以让线程执行 wait 函数,在原地休息,然后被其他线程执行 notify_one 后休眠的线程会从 wait 的位置继续执行
这里有一个细节,进入 wait 的判空需要使用循环,因为有可能出现空唤醒的现象,即被唤醒之后实际没有数据可以获取,如果出现空唤醒的情况使用循环判空会自己休眠
值得注意的是,线程休眠后醒来是从 wait 开始执行的,而不是一整个函数的开始,所以你必须针对从 wait 之后的代码进行梳理,以确保当代码从 wait 函数的位置唤醒时,能不能正常的继续执行或者退出
退出条件
线程函数会在创建线程时被执行一次,这意味则线程总是在这个线程函数中空转或者休眠,你需要将线程函数运行在循环中,否则这个函数会执行结束就立即退出线程并销毁
需要注意的是线程空转会消耗大量CPU资源,所以当没任务的时候最好让线程休眠
通常线程退出意为着一个生产消费模型的类要被销毁了,但是消费的数据大概率没有处理完成,你可以立即停下线程并退出
如果你需要完成任务后在退出,你有两种选择,主线程退出但子线程会执行完任务后再退出,这种情况你需要以 detach 的形式运行 thread 以脱离主线程执行,他会自动回收资源,如果你希望子线程与主线程同时退出,你需要使用 join 来回收线程资源,如果你不回收线程资源则程序大概率会直接崩溃
连续条件判断
你会注意到 queue_th 容器中,pop_safe 函数负责弹出数据,它是将 empty 判空和 front pop 获取数据并弹出一次性完成的,这样做是非常有必要的,因为如果你不把这三行代码写在一个锁内,会出现数据竞争的问题导致数据损坏
设想一下,步骤一,如果你判空时上锁然后解锁,你知道到此时有数据,步骤二,你开始获取数据,步骤三,上锁并弹出数据后解锁你确保了数据一致性,看起来没问题,但是在多线程下,你在步骤一解锁后可能另一条线程就已经率先弹出数据了,然后等你执行到步骤三发现容器是空的,程序崩溃
所以发现问题了吗,多线程下很多日常步骤都会变的危险,你任何需要改变共享数据的行为都需要检查哪一步可能被人捷足先登
管理生命周期
这里还是要提一下智能指针的声明周期问题,factory_eggs 本身没有任何手动管理指针的方式,它借助了 shared_ptr 开管理 thread 的创建和退出,并让 thread 在退出时自动调用 join 回收资源
子线程 thread 的智能智能被放到了 vector 中,这意味着 vector 接管了智能指针的声明周期,只要你不主动释放 vector 内的数据,factory_eggs 类就会在析构函数时自动释放 vector 的数据,从而释放 shared_ptr 的管理权,进一步的智能指针会调用释放函数中的 join 来释放 thread 线程资源,完成整个自动化的线程回收
退出顺序
如果 factory_eggs 使用了 thread 的 join 函数来回收数据,那 factory_eggs 运行在主线程的析构函数就会阻塞,这意味着你必须等待 thread 完全退出之后,factory_eggs 的析构函数才能被退出,所以其实 factory_eggs 的释放比 thread 要晚一点,尽管你从打印信息中看到的是 factory_eggs 析构的打印在 thread 之前
乱码打印
在打印结果上,你可能发生日志是乱糟糟的,其实这是因为多线程下打印的原因,因为日志的输出缓冲区只有一个,在多线程中相当于共享数据,如果没有线程同时向缓冲区内推入数据,就会出现字符重叠的事情,如果我们仔细检查其实发现并没少东西,只是两组字符穿插推入缓冲区而已
静态变量
在 relax_time 函数中,出现一个 static 静态变量修饰的代码,这是变量的基础声明方式,静态变量的内存会被创建之后一直存在,而且不会被重复创建和销毁,直到整个程序退出才会消失
我们可以利用不会重复定义的这一点,这意味着这个变量即使在循环从被反复执行声明,但也只是存在一份数据而已,所以静态变量的数据可以减少创建次数
但是在此之前我却从未提及静态变量,我认为这种数据类型并不重要,它跟全局变量类似,只是存在的声明周期更差长而已,一个不会死亡也不会被重复定义的变量,一块不会消失的内存而已
项目路径
https://github.com/HellowAmy/mcpp.git