如何用C20协程简化异步网络编程,实现零基础深入浅出?

摘要:C++ 20 四大特性之一的协程,是如何简化网络编程复杂性的?何为有栈协程、何为无栈协程?C++20 的协程有何缺点?使用什么协程库能快速接入 C+&
传统异步回调 vs C++20协程 协程是一种函数对象,可以设置锚点做暂停,然后再该锚点恢复继续运行。它是如何应用在网络异步编程方面的,请对比下面的两种代码风格: 基于回调的异步网络编程 先来看一个异步编程的典型例子 (伪代码): async_resolve({host, port}, [](auto endpoint){ async_connect(endpoint, [](auto error_code){ async_handle_shake([](auto error_code){ send_data_ = build_request(); async_write(send_data_, [](auto error_code){ async_read(); }); }); }); }); void async_read() { async_read(response_, [](auto error_code){ if(!finished()) { append_response(recieve_data_); async_read(); }else { std::cout<<"finished ok\n"; } }); } 基于异步回调的 client 流程如下: 异步域名解析 异步连接 异步 SSL 握手 异步发送数据 异步接收数 这个代码有很多回调函数,使用回调的时候还有一些陷阱,比如如何保证安全的回调、如何让异步读实现异步递归调用,如果再结合异步业务逻辑,回调的嵌套层次会更深,我们已经看到callback hell 的影子了!可能也有读者觉得这个程度的异步回调还可以接受,但是如果工程变大,业务逻辑变得更加复杂,回调层次越来越深,维护起来就很困难了。 基于协程的异步网络编程 再来看看用协程是怎么写同样的逻辑 (伪代码): auto endpoint = co_await async_query({host, port}); auto error_code = co_await async_connect(endpoint); error_code = co_await async_handle_shake(); send_data = build_request(); error_code = co_await async_write(send_data); while(true) { co_await async_read(response); if(finished()) { std::cout<<"finished ok\n"; break; } append_response(recieve_data_); } 同样是异步 client,相比回调模式的异步 client,整个代码非常清爽,简单易懂,同时保持了异步的高性能,这就是 C++20 协程的威力! C++ 20 协程提案之争 协程分为无栈协程和有栈协程两种 无栈指可挂起/恢复的函数 有栈协程则相当于用户态线程 有栈协程切换的成本是用户态线程切换的成本,而无栈协程切换的成本则相当于函数调用的成本。 有栈(stackful)协程通常的实现手段是在堆上提前分配一块较大的内存空间(比如 64K),也就是协程所谓的“栈”,参数、return address 等都可以存放在这个“栈”空间上。如果需要协程切换,那么通过 swapcontext 一类的形式来让系统认为这个堆上空间就是普通的栈,这就实现了上下文的切换。 有栈协程最大的优势就是侵入性小,使用起来非常简便,已有的业务代码几乎不需要做什么修改,但是 C++20 最终还是选择了使用无栈协程,主要出于下面这几个方面的考虑: 栈空间的限制 有栈协程的“栈”空间普遍是比较小的,在使用中有栈溢出的风险;而如果让“栈”空间变得很大,对内存空间又是很大的浪费。无栈协程则没有这些限制,既没有溢出的风险,也无需担心内存利用率的问题。 性能 有栈协程在切换时确实比系统线程要轻量,但是和无栈协程相比仍然是偏重的,这一点虽然在我们目前的实际使用中影响没有那么大,但也决定了无栈协程可以用在一些更有意思的场景上。
阅读全文