作为C++协程框架,Coke当然可以优雅地发起计算任务,我们以“计算一组数据的和”为例,先直观地了解一下发起计算任务的方式。

#include <iostream>
#include <vector>
#include <numeric>
#include "coke/coke.h"

long long accumulate(const long long *first, const long long *last) {
    long long r = 0;
    while (first != last) {
        r += *first;
        ++first;
    }

    return r;
}

int main() {
    constexpr int N = 100000;
    std::vector<long long> numbers(N, 0);

    // 将[1, N]填充到numbers当中
    std::iota(numbers.begin(), numbers.end(), 1LL);

    // 将计算任务分成两个子任务
    std::vector<long long> result;
    const long long *begin = numbers.data(), *mid, *end;
    mid = begin + N / 2;
    end = begin + N;

    // 同时发起两个子任务,并同步等待结果
    result = coke::sync_wait(
        coke::go(accumulate, begin, mid),
        coke::go(accumulate, mid, end)
    );

    // 输出计算结果
    long long total = result[0] + result[1];
    std::cout << "1 + ... + " << N << " = " << total << std::endl;

    return 0;
}

创建计算任务就是如此简洁,只需要向coke::go传入可调用对象和参数就可以了。但由于C++中有模板和函数重载,当函数声明比较复杂的时候,使用起来会比较晦涩。此时若恰好在协程上下文中,可以通过coke::switch_go_thread()切换到计算线程,然后直接调用函数就可以了。例如

coke::Task<> switch_example(std::vector<int> v) {
    // std::sort是个模板函数,需指定模板参数才是完整的函数名

    // 无法编译
    // co_await coke::go(std::sort, v.begin(), v.end());
    // 可用,但冗长
    // co_await coke::go(std::sort<std::vector<int>::iterator>, v.begin(), v.end());

    // 当函数有多个模板参数或多个重载时,情况会更加复杂
    // 此时直接切换计算线程,与上述调用效果相同,但更加简单直观
    co_await coke::switch_go_thread();
    std::sort(v.begin(), v.end(0));
}

当然这种方式有一定的限制,比如只能在协程函数中使用、不能用于同时启动多个计算任务等。但当你遇到多个计算任务和其他任务互相穿插的时候,你会再回来考虑它的,因此不必惊慌,仅把它当做语法糖,按个人喜好使用即可。

可为何一定要切换到计算线程呢,在当前线程直接计算可以吗?

在上一篇文章中,我们了解了如何发起网络任务(以Http为例)。一般来说,当网络任务完成时,后续的代码会被调度到一组用于处理该任务的线程组中运行(即C++ Workflow中的handler线程),此时可以对网络任务结果进行简单的处理,发起新的异步任务或者结束当前函数。但如果需要进行复杂的计算任务,直接在handler线程中处理很可能发生阻塞,以至于其他的网络任务无法被及时调度。为了避免这种问题,复杂的计算任务一定要切换到计算任务专属的线程当中去。

下面再通过一个复合的示例来展示其用法

coke::Task<> find_link(const std::string &url) {
    coke::HttpClient cli;
    coke::HttpResult result;

    // 发起Http请求,获取内容
    result = co_await cli.request(url);

    // 简单起见,此处不赘述错误处理的过程,也忽略了Chunk Encoding
    std::string_view body = coke::http_body_view(result.resp);
    std::cout << body << std::endl;

    // 仅当需要的时候才执行切换
    if (body.size() > 1024) {
        co_await coke::switch_go_thread();

        std::regex url_re(R"url_re(https?://[a-zA-Z0-9\./\?=_-]+)url_re");
        std::cregex_iterator it(body.begin(), body.end(), url_re), end;

        while (it != end) {
            std::cmatch match = *it;
            std::cout << match.str() << std::endl;
            ++it;
        }

        // ... 其他复杂计算任务
    }
    else {
        std::cout << "ignore small page" << std::endl;
    }
}

在项目示例中,使用一个归并排序的例子展示了异步递归计算任务的使用方法。

本系列文章同步发布于个人网站知乎专栏,转载请注明来源,以便读者及时获取最新内容及勘误。

发表回复

您的电子邮箱地址不会被公开。 必填项已用*标注