asio boost 异步错误处理_C++20 await & asio 相关的学习

本文探讨了使用asio库配合C++20的协程特性进行异步错误处理的方法。作者旨在学习和推广asio,期望通过统一的库减少开发者在处理线程、同步、异步等问题上的困扰。文中介绍了如何实现同步非阻塞RPC,包括Channel、Mutex和WaitGroup等基础组件的简易实现,并进行了性能测试。此外,文章还提出了开发协程库的挑战以及在协程编程中可能遇到的问题,如忘记使用co_await可能导致的并发问题。最后,建议阅读cppcoro作者关于异步转移的博客文章,并期待更好的协程调试支持。
摘要由CSDN通过智能技术生成

背景:

最新版的asio(1-13-0)的api添加了对co_await的支持 (之前的版本也支持一些,只不过命名空间还没有正式放入asio)。

我打算学习下asio,也打算抛弃自己的网络库了,也期望其他的库或者driver能够统一的使用asio+co_await,这样我们应用程序开发者就能快速的引入库(少一些心智负担,不用担心线程/同步/异步/生命周期管理等各种模型差异性导致的顾虑)。[当然我也并不是说asio就是最好的选择,但是大家都基于某个统一的库去做开发,还是有很多好处的]。

目的:

我的目的是采用 asio+await开发一个同步非阻塞RPC(嗯,就是类似golang grpc),来替换调我的异步RPC,进而通过它再重新开发Orleans-CPP简易版本。

通过我个人理解,要实现同步RPC(当然也包括其他基于协程的应用)我们需要先实现几个基础功能):

  1. Channel - 用于实现协程之间的pipeline
  2. Mutex - 基于协程的锁
  3. WaitGroup - 基于协程的等待对象
  4. CondVariables
  5. ……(带补充)

因为同步RPC库(支持多个协程使用同一个RPC对象去调用服务)的实现(个人理解,我本打看看grpc怎么做的,但时间还不够,所以~~~)大概如下:

开启一个writer协程,使用channel收集其他协程(调用RPC函数时)产生的消息(消息里包括一个request msg、一个用于接收此次请求的response的channel),开启一个receive协程,来接收服务器的网络消息,并且从中解析各个respnse,并且根据其seqid (通过锁)拿到其对应的调用者的channel,然后将response放入channel,以此唤醒调用者。

简要实现:

下面是我写的简易版channel和协程锁的实现(我这几天才开始学习asio,所以难免有一些问题或者错误处理省略了,还请指教)

//
// echo_server.cpp
// ~~~~~~~~~~~~~~~
//
// Copyright (c) 2003-2019 Christopher M. Kohlhoff (chris at kohlhoff dot com)
//
// Distributed under the Boost Software License, Version 1.0. (See accompanying
// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt)
//

#include <cstdio>
#include <queue>
#include <mutex>
#include <iostream>

#include <asio/co_spawn.hpp>
#include <asio/detached.hpp>
#include <asio/io_context.hpp>
#include <asio/ip/tcp.hpp>
#include <asio/signal_set.hpp>
#include <asio/write.hpp>
#include <asio/awaitable.hpp>
#include <asio/redirect_error.hpp>

using asio::ip::tcp;
using asio::awaitable;
using asio::co_spawn;
using asio::detached;
using asio::use_awaitable;
using asio::redirect_error;
namespace this_coro = asio::this_coro;

template<typename T>
class Channel : public asio::noncopyable
{
public:
    using Ptr = std::shared_ptr<Channel<T>>;
    using Container = std::queue<T>;

    static Ptr Make(asio::io_context& context)
    {
        struct make_shared_enabler : public Channel<T>
        {
            make_shared_enabler(asio::io_context& context)
                :
                Channel<T>(context)
            {}
        };
        return std::make_shared<make_shared_enabler>(context);
    }

    asio::io_context& context()
    {
        return mContext;
    }

    auto& operator << (T value)
    {
        {
            std::lock_guard<std::mutex> lck(mValuesGuard);
            mValues.push(std::move(value));
        }
        mTimer.cancel_one();
        return *this;
    }

    asio::awaitable<T> takeOne()
    {
        while (true)
        {
            {
                std::lock_guard<std::mutex> lck(mValuesGuard);
                if (!mValues.empty())
                {
                    auto result = mValues.front();
                    mValues.pop();
                    co_return result;
                }
            }

            asio::error_code ec;
            co_await mTimer.async_wait(redirect_error(use_awaitable, ec));
        }
    }

    asio::awaitable<Container>  takeAll()
    {
        while (true)
        {
            {
                std::lock_guard<std::mutex> lck(mValuesGuard);
                if (!mValues.empty())
                {
                    co_return std::move(mValues);
                }
            }

            asio::error_code ec;
            co_await mTimer.async_wait(redirect_error(use_awaitable, ec));
        }
    }

private:
    Channel(asio::io_context& context)
        :
        mContext(context),
        mTimer(context)
    {
        mTimer.expires_at(std::chrono::steady_clock::time_point::max());
    }

private:
    asio::io_context&   mContext;
    asio::steady_timer  mTimer;

    Container           mValues;
    std::mutex          mValuesGuard;
};

class CoroutineMutex : public asio::noncopyable
{
public:
    using Ptr = std::shared_ptr<CoroutineMutex>;

    static Ptr Make(asio::io_context& context)
    {
        struct make_shared_enabler : public CoroutineMutex
        {
            make_shared_enabler(asio::io_context& context)
                :
                CoroutineMutex(context)
            {}
        };
        return std::make_shared<make_shared_enabler>(context);
    }

    asio::io_context& context()
    {
        return mContext;
    }

    awaitable<void> lock()
    {
        asio::error_code ec;
        while (mLocked.exchange(true))
        {
            co_await mTimer.async_wait(redirect_error(use_awaitable, ec));
        }
    }

    void unlock()
    {
        auto o = mLocked.exchange(false);
        assert(o);
        mTimer.cancel_one();
    }

private:
    CoroutineMutex(asio::io_context& context)
        :
        mLocked(false),
        mContext(context),
        mTimer(context)
    {
    }
    virtual ~CoroutineMutex() = default;

private:
    std::atomic_bool    mLocked;
    asio::io_context&   mContext;
    asio::steady_timer  mTimer;
};

static awaitable<void> producer(Channel<int>::Ptr channel)
{
    asio::steady_timer timer(channel->context());

    int i = 0;

    while (true)
    {
        timer.expires_from_now(std::chrono::seconds(1));
        asio::error_code ec;
        co_await timer.async_wait(redirect_error(use_awaitable, ec));

        (*channel) << i++;
    }
}

static awaitable<void> consumer(Channel<int>::Ptr channel)
{
    while (true)
    {
        {
            auto i = co_await channel->takeOne();
            std::cout << i << std::endl;
        }
        {
            auto v = co_await channel->takeAll();
            for (; !v.empty();)
            {
                std::cout << v.front() << std::endl;
                v.pop();
            }
        }
    }
}

std::atomic<int> global = 0;

static awaitable<void> testMutex(CoroutineMutex::Ptr mutex, int index, Channel<int>::Ptr channel)
{
    asio::steady_timer timer(mutex->context());

    for (int i = 0; i < 10; i++)
    {
        timer.expires_from_now(std::chrono::milliseconds(100+std::rand()%100));
        asio::error_code ec;
        co_await timer.async_wait(redirect_error(use_awaitable, ec));

        co_await mutex->lock();
        global++;
        std::cout << "in " << index << " take global is:" << global << std::endl;
        (*channel) << global;
        mutex->unlock();
    }
}

static awaitable<void> consumerGlobalChange(Channel<int>::Ptr channel)
{
    while (true)
    {
        auto currentGlobalValue = co_await channel->takeOne();
        std::cout << "current currentGlobalValue is :" << currentGlobalValue << std::endl;
    }
}

int main()
{
    try
    {
        asio::io_context io_context(1);

        asio::signal_set signals(io_context, SIGINT, SIGTERM);
        signals.async_wait([&](auto, auto) { io_context.stop(); });

        {
            // 一个生产者协程和两个消费者协程
            auto channel = Channel<int>::Make(io_context);
            co_spawn(io_context, [=]() {
                return producer(channel);
            }, detached);
            co_spawn(io_context, [=]() {
                return consumer(channel);
            }, detached);
            co_spawn(io_context, [=]() {
                return consumer(channel);
            }, detached);
        }
        
        {
            // 构造10个协程通过互斥进行修改全局变量
            // 并在其中将全局变量当前的值放入channel
            // 开启一个协程通过channel去观察全局变量的变更
            auto mutex = CoroutineMutex::Make(io_context);
            auto consumerChannel = Channel<int>::Make(io_context);

            for (int i = 0; i < 10; i++)
            {
                co_spawn(io_context, [=]() {
                    return testMutex(mutex, i, consumerChannel);
                }, detached);
            }
            co_spawn(io_context, [=]() {
                return consumerGlobalChange(consumerChannel);
            }, detached);
        }
        
        io_context.run();
    }
    catch (std::exception & e)
    {
        std::printf("Exception: %sn", e.what());
    }
}

性能测试:

构造一百万个协程,并且使用一百万个channel将它们两两串连起来,然后做类似击鼓传花的操作。从第一个协程开始输入,直到最后一个协程收到,代码如下:

const static auto MaxNum = 1000000;

static awaitable<void> foo(Channel<int>::Ptr in, Channel<int>::Ptr out)
{
    auto value = co_await in->takeOne();
    // 将收到的值+1传递给下一位伙伴
    (*out << (value + 1));
    if (value == MaxNum)
    {
        std::cout << "end" << std::endl;
        out->context().stop();
    }
}

static awaitable<void> start(Channel<int>::Ptr out)
{
    (*out << 1);
    co_return;
}

auto in = Channel<int>::Make(io_context);
auto out = Channel<int>::Make(io_context);

// 开启一个协程做初始输入动作
co_spawn(io_context, [=]() {
    return start(in);
}, detached);

for (int i = 0; i < MaxNum; i++)
{
    co_spawn(io_context, [=]() {
        return foo(in, out);
    }, detached);

    in = out;
    out = Channel<int>::Make(io_context);
}

在 的垃圾I5 Win10机器上,花费8s跑完整个程序,(程序只有一个io_context,所以是单线程)占满单个CPU核心,内存占用最高峰为1G。

注:asio在开启协程后,需要在下一次调度时才执行协程,awaituv则是立马执行协程,所以asio这种做法在我的测试中相比awaituv所花费的时间(我猜测)可能是数量级的差异,当然这也没太大意义,在真实场景下,协程并不是执行一次就马上结束,它通常会存活一段时间的。

(刚才写了一个Golang的版本,发现也是立即执行,只需要1s时间就跑完测试程序。可是一旦我将初始输入动作放在开启击鼓传花的协程动作之后(也即是说哪怕它们本身立即执行,但它们无法立即完成,所以必须等待/存活),时间耗费多少呢?抱歉,我没跑出结果就强制关闭了程序(进度也尚不可知),此时内存耗费在2.9G。

当然,这个测试程序并不能完全说明channel本身的性能,或许更多的是测试协程调度的性能?

疑问/注意

  1. 要开发一个支持co_await的库(类似最新版asio或者 awaituv)是非常困难的,我期望能够有相当背景的公司开发一个通用的封装,来支持其他开发者将异步调用转换为同步调用(就类似awaituv里做过的一些事情/以及封装,但它有bug或者功能还很简陋·····)。 cppcoro是一个选择,可惜它又干了一些io/network相关事情(而且还没干好~~),而且~~~作者我不了解啊,我觉得背书不能啊)
  2. 如果一个函数是协程函数,但你调用时忘记co_await了,那就是开启一个新协程,估计这容易成为一个坑?哈哈哈
  3. 我尝试通过RAII做一个协程锁的guard,但是构造函数里不能co_await mutex->lock(); 相当尴尬,因为operator new、构造函数 等编译器自动生成的函数都不是awaitable的啊~~~但如果它们是awiatable,那完蛋了,所有对他们的调用你基本上都得(不能忘记)加co_await。
  4. co_await 可以理解为一个操作符,或者把它当成一个函数?它接收的类型是awaitable,那我就不容易做链式调用啊:co_await channel >> a >> b >> c; (假设我们有一个全局函数 awaitable<Channel<T>> operator >> (Channel<T>, T);),直观上看,我们应该让 Channel<T>本身是一个awaitable对象 ,但我还没搞定它,算了,先赶紧发布文章请 @vczh 帮忙点赞再说吧。
  5. Asymmetric Transfer cppcoro作者的这个blog里的三篇文章推荐大家看看。
  6. 现在VS调试协程程序时,在协程函数里无法查看函数参数(函数栈变量则可以查看)·····希望以后能得到支持。
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值