「这是我参与11月更文挑战的第 3 天,活动详情查看:2021最后一次更文挑战」。
这几天看到一篇文章 —— 《写了一段高端 C++ 代码》 ,其中用到的代码和我之前所看到的一篇 《【转载】走进 C++11(三十一) 如何用 60 行实现 C++11 thread pool》 有几分相似【但前者的目的不是为了实现线程池,而是为一个(GL)子线程分配不同的任务的优雅清爽写法】,针对该篇文章我写过一篇文章《C++11 实现优雅的线程池》对代码细节进行了详细的解释。那么今天也顺便对这篇近似的代码也进行一下详细解释(原文代码几乎没有注释),一来作为自己的学习笔记,方便日后回顾,二来方便对 C++ 11 不熟悉的同学了解一下相关知识点。
背景介绍
在音视频方向中,线程分为普通线程和 GL 线程(OpenGL 线程),所有 GL 相关语句都要在 GL 线程中执行;在普通线程中,只能执行那些我们的普通语句。
假设在具体项目开发中有这样一各需求:在普通线程中正在执行任务 1 ,突然想要执行某些必须要在 GL 线程下执行的任务 2 (比如某些初始化工作,初始化某些 GL 相关的对象),执行完此任务 2 后又继续执行自己的任务 3 ,像在同一个线程执行一样:
void func()
{
task1();
task2(); ///< 需要在 GL 线程执行
task3();
}
这里有个关键点:task3() 一定要等到 task2() 执行完毕后才可执行,但是由于task2() 是在其他线程运行,有没有什么技术可以起到阻塞执行的效果呢?
在学习 C++11 之前的话,我们会很自然地想到使用条件变量:
普通线程在 task2() 后使用 wait() 阻塞普通线程,待 GL 线程中的任务执行完后使用 notity() 通知普通线程结束等待,则可达到顺序执行的目的。
void task2()
{
...
notify();
}
void func()
{
task1();
task2(); ///< 需要在 GL 线程执行
wait();
task3();
}
在学习 C++11 之后呢,我们则可以利用 future 达到阻塞的目的,并很方便地获取到函数的执行结果(其实内部还是会用到条件变量,但是使得主线程中逻辑更清晰直观)。
关于 future 的具体定义与用法可以参考我之前整理的一篇文章《【转载】走进 C++11(二十七)处理未来发生的事情 std::future》,原作者举得例子通俗易懂,我在他的基础上进行了补充和细化。
现在我们回到正题,来看看如何通过 C++11 来实现,代码如下:
#include <atomic>
#include <chrono>
#include <condition_variable>
#include <functional>
#include <future>
#include <iostream>
#include <memory>
#include <mutex>
#include <queue>
#include <thread>
#include <vector>
class GLTaskDispatch {
public:
/// @note 获取单例
static GLTaskDispatch& getInstance() {
static GLTaskDispatch t; ///< 静态局部变量
return t;
}
/// @note 顺序执行任务队列中的任务
bool start() {
auto func = [this]() { ///< 子线程执行函数
while (!_interrupt.load()) {
/// @note
std::function<void()> task;
{
std::unique_lock<std::mutex> lock(this->_taskMutex);
/// @note 收到解除阻塞的通知,并且断言(Predicate)条件为真的时候才会继续执行
/// 即 中断条件为真 或者 任务队列不为空
this->_taskCv.wait(lock, [this]
{ return this->_interrupt.load() || !this->_tasks.empty(); });
/// @note 如果中断条件为真,则跳过后面的任务执行
if (this->_interrupt.load()) {
continue;
}
/// 取队首任务,并出队
task = std::move(this->_tasks.front());
this->_tasks.pop();
}
/// @note 执行任务
task();
}
};
/// @note 构造一个线程并指定执行函数
_thread = std::make_unique<std::thread>(func);
return true;
}
/// @note 任务结束
bool stop() {
_interrupt.store(true); ///< 打断正在执行的任务
this->_taskCv.notify_all(); ///< 通知所有等待条件变量的子线程
if (_thread && _thread->joinable()) {
_thread->join();
}
return true;
}
/// @note 可变参模板函数【尾随返回类型】
/// 将函数(可调用实体)打包为任务放进任务队列,并返回 future 以便查询任务执行结果
template <typename F, typename... Args>
auto run(F&& f, Args &&...args)
-> std::shared_ptr<std::future<std::result_of_t<F(Args...)>>> { ///< 返回值类型
/// @note 类型重定义
using returnType = std::result_of_t<F(Args...)>; ///< 推导出可调用对象的返回值类型
/// @note 将外部传进来的可调用对象及其参数打包
auto task = std::make_shared<std::packaged_task<returnType()>>(
std::bind(std::forward<F>(f), std::forward<Args>(args)...));
std::future<returnType> ret = task->get_future();
{
std::lock_guard<std::mutex> lock(this->_taskMutex);
std::cout << "_tasks emplace !" << std::endl;
this->_tasks.emplace([task]() {
(*task)();
}); ///< 将打包的 task 以 lambda 表达式的形式(满足 function<void()> )进行封装(在内部调用 task ),并将这个封装的可调用实体存进任务队列当中
}
this->_taskCv.notify_all(); ///< 通知所有等待条件变量的子线程
/// @note 返回 future 给函数调用者,以便获取返回值
return std::make_shared<std::future<std::result_of_t<F(Args...)>>>(
std::move(ret));
}
private:
GLTaskDispatch() {}
std::unique_ptr<std::thread> _thread = nullptr;
std::atomic<bool> _interrupt{ false }; ///< 标记是否中断的原子变量
std::queue<std::function<void()>> _tasks; ///< 任务队列,注意封装的是 function<void()>
std::mutex _taskMutex; ///< 任务队列锁
std::condition_variable _taskCv;
};
void func1() {
for (int i = 0; i < 20; i++) {
std::cout << "func1 " << i << "\n";
}
}
int func2() {
for (int i = 0; i < 20; i++) {
std::cout << "func2 " << i << "\n";
}
return 64;
}
int main() {
GLTaskDispatch& t = GLTaskDispatch::getInstance();
t.start(); ///< 启动唯一的 GL 子线程
/// @note GL 子线程添加了 func1 任务,并 get 等待其执行完毕
t.run(func1)->get();
std::cout << "func1 return" << std::endl;
/// @note GL 子线程添加了 func2 任务,并 get 等待其执行完毕
int d = t.run(func2)->get();
std::cout << "func2 return " << d << std::endl;
/// @note 主线程干点别的任务
std::this_thread::sleep_for(std::chrono::seconds(2));
t.stop(); ///< GL 子线程终止
return 0;
}
输出的结果为
值得注意的时, func2 任务在 func1 执行结束后才添加到了任务队列中。
如果在添加 func1 时没有立即 get 的话,则输出结果如下
可见 func2 任务在 func1 执行结束前就添加到了任务队列中。