C++通信新特性协程详细介绍

2022-11-13 19:11:19 通信 详细介绍 新特性

一、关于协程

从 1.54.0 版本开始,Boost.Asio 支持协程。虽然您可以直接使用 Boost.Coroutine,但 Boost.Asio 中对协程的显式支持使得使用它们变得更加容易。

协程让您创建一个反映实际程序逻辑的结构。异步操作不会拆分函数,因为没有处理程序来定义异步操作完成时应该发生什么。程序可以使用顺序结构,而不是让处理程序相互调用。

二、协程的好处

考虑多任务协作的场景. 如果是线程的并发, 那么大家需要抢 CPU 用, 还需要条件变量/信号量或者上等技术, 来确保正确的线程正在工作.

如果在协程中, 大家就可以主动暂停自己, 多个任务互相协作. 这样可能就比大家一起抢 CPU 更高效一点, 因为你能够控制哪个协程用上 CPU.

一个例子:

生产者/消费者模型: 生产者生产完毕后, 暂停自己, 把控制流还给消费者. 消费者消费完毕后, resume 生产者, 生产者继续生产. 这样循环往复.

异步调用: 比如你要请求网络上的一个资源.

  • 发请求给协程
  • 协程收到请求以后, 发出请求. 协程暂停自己, 把控制权还回去.
  • 你继续做些别的事情. 比如发出下一个请求. 或者做一些计算.
  • 恢复这个协程, 拿到资源 (可能还要再等一等)

理想状态下, 4 可以直接用上资源, 这样就完全不浪费时间.

如果是同步的话:

  • 发请求给函数.
  • 函数收到请求以后, 等资源.
  • 等了很久, 资源到了, 把控制权还回去.

明显需要多等待一会儿. 如果需要发送上百个请求, 那显然是第一种异步调用快一点. (等待的过程中可以发送新的请求)

如果没有协程的话, 解决方案之一是使用多线程. 像这样:

  • 发请求给函数.
  • 函数在另外的线程等, 不阻塞你的线程.
  • 你继续做些别的事情. 比如发出下一个请求. 或者做一些计算.
  • 等到终于等到了, 他再想一些办法通知你.

然后通知的办法就有 promise 和回调这些办法.

三、协程得用法

我们照着 c++20 标准来看看怎么用协程. 用 g++, 版本 10.2 进行测试.

目前 C++20 标准只加入了协程的基本功能, 还没有直接能上手用的类. GCC 说会尽量与 clang MSVC 保持协程的 ABI 兼容, 同时和 libc++ 等保持库的兼容. 所以本文可能也适用于它们.

协程和主程序之间通过 promise 进行通信. promise 可以理解成一个管道, 协程和其调用方都能看得到.

以前的 std::async std::future 也是基于一种特殊的 promise 进行通信的, 就是 std::promise. 如果要使用协程, 则需要自己实现一个全新的 promise 类, 原理上是类似的.

四、与线程的区别

线程处于进程之中,协程处于线程之中,线程有系统内核调度,而协程有程序员自己调度。一个线程可以有多个协程,而且只要内存足够,一个线程中可以有任意多个协程;但某一时刻只能有一个协程在运行,多个协程分享该线程分配到的计算机资源。协程是追求极限性能和优美的代码结构的产物。

使用过程中需要包含#include <boost/coroutine2/all.hpp>,链接动态库:-lboost_coroutine -lboost_context。关于使用boost库错

协程有如下特点:

  1. 同其他数据类型一样,协程也是第一类(first-class)对象,可以被当参数传递等操作;
  2. 运行特点是挂起运行,离开协程,过后再进入,恢复运行;
  3. 具有对称和非对称的转移控制机制;
  4. 挂起前和恢复后本地变量的值是一致的;
  5. 有stackless和stackful两种类型

五、协程示例

示例 32.7。使用 Boost.Asio 的协程

#include <boost/asio/io_service.hpp>
#include <boost/asio/spawn.hpp>
#include <boost/asio/write.hpp>
#include <boost/asio/buffer.hpp>
#include <boost/asio/ip/tcp.hpp>
#include <list>
#include <string>
#include <ctime>
using namespace boost::asio;
using namespace boost::asio::ip;
io_service iOService;
tcp::endpoint tcp_endpoint{tcp::v4(), 2014};
tcp::acceptor tcp_acceptor{ioservice, tcp_endpoint};
std::list<tcp::Socket> tcp_sockets;
void do_write(tcp::socket &tcp_socket, yield_context yield)
{
  std::time_t now = std::time(nullptr);
  std::string data = std::ctime(&now);
  async_write(tcp_socket, buffer(data), yield);
  tcp_socket.shutdown(tcp::socket::shutdown_send);
}
void do_accept(yield_context yield)
{
  for (int i = 0; i < 2; ++i)
  {
    tcp_sockets.emplace_back(ioservice);
    tcp_acceptor.async_accept(tcp_sockets.back(), yield);
    spawn(ioservice, [](yield_context yield)
      { do_write(tcp_sockets.back(), yield); });
  }
}
int main()
{
  tcp_acceptor.listen();
  spawn(ioservice, do_accept);
  ioservice.run();
}

调用 Boost.Asio 使用协程的函数是 boost::asio::spawn()。传递的第一个参数必须是 I/O 服务对象。第二个参数是将成为协程的函数。此函数必须接受 boost::asio::yield_context 类型的对象作为其唯一参数。它必须没有返回值。示例 32.7 使用 do_accept() 和 do_write() 作为协程。如果函数签名不同,例如 do_write() 的情况,您必须使用类似 std::bind 的适配器或 lambda 函数。

您可以将 boost::asio::yield_context 类型的对象而不是处理程序传递给异步函数。 do_accept() 将参数 yield 传递给 async_accept()。在 do_write() 中,yield 被传递给 async_write()。这些函数调用仍会启动异步操作,但在操作完成时不会调用任何处理程序。而是恢复启动异步操作的上下文。当这些异步操作完成时,程序会从中断的地方继续。

do_accept() 包含一个 for 循环。每次调用该函数时,都会将一个新套接字传递给 async_accept()。一旦客户端建立连接,do_write() 将作为协程调用,并带有 boost::asio::spawn() 以将当前时间发送给客户端。

for 循环可以很容易地看出程序在退出之前可以为两个客户端提供服务。由于该示例基于协程,因此可以在 for 循环中实现异步操作的重复执行。这提高了程序的可读性,因为您不必跟踪对处理程序的潜在调用来找出最后一个异步操作何时完成。如果时间服务器需要支持两个以上的客户端,则只需调整 for 循环。

到此这篇关于C++通信新特性协程详细介绍的文章就介绍到这了,更多相关C++协程内容请搜索以前的文章或继续浏览下面的相关文章希望大家以后多多支持!

相关文章