C++11 为一个子线程分配不同的任务的优雅写法

1,150 阅读3分钟

「这是我参与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;
}

输出的结果为

image.png

值得注意的时, func2 任务在 func1 执行结束后才添加到了任务队列中。

如果在添加 func1 时没有立即 get 的话,则输出结果如下

image.png

可见 func2 任务在 func1 执行结束前就添加到了任务队列中。