{"id":357885,"date":"2024-05-21T00:00:04","date_gmt":"2024-05-21T00:00:04","guid":{"rendered":"http:\/\/savepearlharbor.com\/?p=357885"},"modified":"-0001-11-30T00:00:00","modified_gmt":"2024-05-21T00:00:04","slug":"","status":"publish","type":"post","link":"https:\/\/savepearlharbor.com\/?p=357885","title":{"rendered":"<span>High-performance network library using C++20 coroutines<\/span>"},"content":{"rendered":"<div><!--[--><!--]--><\/div>\n<div id=\"post-content-body\">\n<div>\n<div class=\"article-formatted-body article-formatted-body article-formatted-body_version-2\">\n<div xmlns=\"http:\/\/www.w3.org\/1999\/xhtml\">\n<figure class=\"full-width\"><img loading=\"lazy\" decoding=\"async\" src=\"https:\/\/habrastorage.org\/r\/w780q1\/getpro\/habr\/upload_files\/7f0\/a62\/fc7\/7f0a62fc711201bc7dc0a4fcf288ff6a.jpg\" width=\"1280\" height=\"698\" data-src=\"https:\/\/habrastorage.org\/getpro\/habr\/upload_files\/7f0\/a62\/fc7\/7f0a62fc711201bc7dc0a4fcf288ff6a.jpg\" data-blurred=\"true\"\/><\/figure>\n<p>Asynchronous programming is commonly employed for efficient implementation of network interactions in C++. The essence of this approach lies in the fact that the results of socket read\/write functions are not immediately available but become accessible after some time. This approach allows for loading the processor with useful work during the wait for data. Various implementations of this approach exist, such as <a href=\"https:\/\/en.wikipedia.org\/wiki\/Callback_(computer_programming)\" rel=\"noopener noreferrer nofollow\">callbacks<\/a>, <a href=\"https:\/\/en.wikipedia.org\/wiki\/Actor_model\" rel=\"noopener noreferrer nofollow\">actors<\/a>, <a href=\"https:\/\/en.wikipedia.org\/wiki\/Futures_and_promises\" rel=\"noopener noreferrer nofollow\">future\/promise<\/a>, <a href=\"https:\/\/en.wikipedia.org\/wiki\/Coroutine\" rel=\"noopener noreferrer nofollow\">coroutines<\/a>. In C++, these implementations are available as libraries from third-party developers or can be implemented independently.<\/p>\n<p>Coroutines are the most challenging to implement as they require writing platform-dependent code. However, the recent version of the C++ 20 language standard introduces support for coroutines at the compiler and standard library levels. Coroutines are functions that can suspend their execution, preserving their state, and later return to that state to resume the function&#8217;s work. The compiler automatically creates a checkpoint with the coroutine&#8217;s state.<\/p>\n<p>For a comprehensive understanding of C++ 20 coroutines, refer to this article. Below, we examine a code example using coroutines and describe important points applied during implementation.<\/p>\n<p>For a complete understanding and assimilation of the material presented in this article, it is necessary for the reader to possess basic knowledge in the field of sockets and the TCP\/IP protocol. Fundamental concepts, such as mechanisms of interaction through sockets, principles of data transmission via the TCP\/IP protocol, as well as an understanding of key terms in this domain, will be crucial for the successful comprehension of the provided information. GeeksforGeeks offers useful tutorials on <a href=\"https:\/\/www.geeksforgeeks.org\/tcp-server-client-implementation-in-c\/\" rel=\"noopener noreferrer nofollow\">TCP Server-Client Implementation in C++<\/a> and <a href=\"https:\/\/www.geeksforgeeks.org\/socket-programming-cc\/\" rel=\"noopener noreferrer nofollow\">Socket Programming in C\/C++<\/a>, which can serve as valuable resources for enhancing your understanding of these foundational concepts.<\/p>\n<p>The code of the library discussed below, along with usage examples, can be downloaded from the following link: <a href=\"https:\/\/github.com\/resetius\/coroio\" rel=\"noopener noreferrer nofollow\">https:\/\/github.com\/resetius\/coroio<\/a>.<\/p>\n<h3>Contents<\/h3>\n<ol>\n<li>\n<p><a href=\"#1\" rel=\"noopener noreferrer nofollow\">Network Interaction: Echo Client and Server Example<\/a><\/p>\n<\/li>\n<li>\n<p><a href=\"#2\" rel=\"noopener noreferrer nofollow\">Echo Client and Server Code Overview<\/a><\/p>\n<\/li>\n<li>\n<p><a href=\"#3\" rel=\"noopener noreferrer nofollow\">promise and coroutine_handle<\/a><\/p>\n<\/li>\n<li>\n<p><a href=\"#4\" rel=\"noopener noreferrer nofollow\">Using final_suspend in Unit Tests<\/a><\/p>\n<\/li>\n<li>\n<p><a href=\"#5\" rel=\"noopener noreferrer nofollow\">Event Handling Using select<\/a><\/p>\n<\/li>\n<li>\n<p><a href=\"#6\" rel=\"noopener noreferrer nofollow\">Performance and benchmarks<\/a><\/p>\n<\/li>\n<\/ol>\n<hr\/>\n<p><a class=\"anchor\" name=\"1\" id=\"1\"><\/a><\/p>\n<h3>Network Interaction: Echo Client and Server Example<\/h3>\n<p>Let&#8217;s examine a classic example of network interaction\u2014the implementation of an echo client and server. This example illustrates the task of implementing network programs and effectively demonstrates the details of a specific approach used for asynchronous programming. The echo client\/server example is often included in the documentation of third-party libraries.<\/p>\n<p>The echo server receives a message from the client and immediately sends it back unchanged as a response. The echo client sends a message to the server and waits for a response. Here&#8217;s an example of the process.<\/p>\n<p>This is a client session:<\/p>\n<pre><code class=\"bash\">$ .\/echoclient test Received: test  message Received: message  yet another message Received: yet another message <\/code><\/pre>\n<p>This is a server session:<\/p>\n<pre><code class=\"bash\">$ .\/echoserver Received: test  Received: message  Received: yet another message <\/code><\/pre>\n<p><a class=\"anchor\" name=\"2\" id=\"2\"><\/a><\/p>\n<h3>Echo Client and Server Code Overview<\/h3>\n<p>Let&#8217;s examine the Echo client and server code from top to bottom. We&#8217;ll start<br \/> by looking at the overall code structure, then move on to exploring the C++<br \/> coroutine API, and finally, we&#8217;ll descend to the POSIX API level for socket<br \/> operations. It&#8217;s worth noting that no third-party libraries are used in this<br \/> implementation. Only the standard C++ library and the POSIX API are employed.<\/p>\n<p>The Echo client reads from standard input, sends the input to the socket,<br \/> and waits for a response:<\/p>\n<pre><code class=\"cpp\">TSimpleTask client(TLoop* loop) {     char out[128] = {0};     char in[128] = {0};     ssize_t size = 1;      try {         TSocket input{TAddress{}, 0 \/* stdin *\/, loop->Poller()};         TSocket socket{TAddress{\"127.0.0.1\", 8888}, loop->Poller()};          co_await socket.Connect();         while (size &amp;&amp; (size = co_await input.ReadSome(out, sizeof(out)))) {             co_await socket.WriteSome(out, size);             size = co_await socket.ReadSome(in, sizeof(in));             std::cout &lt;&lt; \"Received: \" &lt;&lt; std::string_view(in, size) &lt;&lt; \"\\n\";         }     } catch (const std::exception&amp; ex) {         std::cout &lt;&lt; \"Exception: \" &lt;&lt; ex.what() &lt;&lt; \"\\n\";     }     loop->Stop();     co_return; }  int main(int argc, char** argv) {     TLoop loop;     client(&amp;loop);     loop.Loop();     return 0; } <\/code><\/pre>\n<p>The Echo server listens on a port, accepts incoming connections (calls<br \/> <code>accept<\/code>), and for each connection, a separate coroutine is created. This<br \/> coroutine reads data from the socket and immediately sends the read data back<br \/> to the socket:<\/p>\n<pre><code class=\"cpp\">TSimpleTask client_handler(TSocket socket, TLoop* loop) {     char buffer[128] = {0}; ssize_t size = 0;      try {         while ((size = co_await socket.ReadSome(buffer, sizeof(buffer))) > 0) {             std::cerr &lt;&lt; \"Received: \" &lt;&lt; std::string_view(buffer, size) &lt;&lt; \"\\n\";             co_await socket.WriteSome(buffer, size);         }     } catch (const std::exception&amp; ex) {         std::cerr &lt;&lt; \"Exception: \" &lt;&lt; ex.what() &lt;&lt; \"\\n\";     }     co_return; }  TSimpleTask server(TLoop* loop) {     TSocket socket(TAddress{\"0.0.0.0\", 8888}, loop->Poller());     socket.Bind();     socket.Listen();      while (true) {         auto client = co_await socket.Accept();         client_handler(std::move(client), loop);     }     co_return; }  int main(int argc, char** argv) {     TLoop loop;     server(&amp;loop);     loop.Loop();     return 0; } <\/code><\/pre>\n<p>The client and server share a similar structure: the <code>main<\/code> function starts an<br \/> event handling loop (<code>TLoop<\/code>) and launches a coroutine. The event handling loop<br \/> interacts with an object (<code>TPoller<\/code>), which performs input-output multiplexing.<br \/> In this implementation, input-output multiplexing is achieved using the <code>select<\/code><br \/> mechanism, and changing it to <code>poll<\/code>, <code>epoll<\/code>, <code>kqueue<\/code>, etc., should not pose a<br \/> problem. <code>TPoller<\/code> waits for events on the socket or a timeout, passes these<br \/> events to the <code>TLoop<\/code> loop, which, in turn, wakes up the coroutines subscribed to<br \/> these events.<\/p>\n<p>The following diagram illustrates how the program switches the execution flow between the<br \/> <code>client<\/code> coroutine and the <code>Loop.loop()<\/code> function when the coroutine sleeps on<br \/> each <code>co_await<\/code> operator call (in reality, it may not sleep every time;<br \/> details below).<\/p>\n<figure class=\"\"><img loading=\"lazy\" decoding=\"async\" src=\"https:\/\/habrastorage.org\/r\/w1560\/getpro\/habr\/upload_files\/89d\/05d\/840\/89d05d840124ada46b9afd8279d892e2.png\" alt=\"coroutine suspend\/resume\" title=\"coroutine suspend\/resume\" width=\"501\" height=\"676\" data-src=\"https:\/\/habrastorage.org\/getpro\/habr\/upload_files\/89d\/05d\/840\/89d05d840124ada46b9afd8279d892e2.png\"\/><\/p>\n<div><figcaption>coroutine suspend\/resume<\/figcaption><\/div>\n<\/figure>\n<p>Below is a diagram of the main classes implemented in our library. <code>TPoller<\/code> has several implementations (<code>select<\/code>, <code>poll<\/code>, <code>epoll<\/code>, <code>kqueue<\/code>, <code>uring<\/code>). <code>TLoop<\/code> contains <code>TPoller<\/code> &#8212; its responsibility is to initiate the <code>Poll<\/code>. <code>TSocket<\/code> contains a reference to <code>TPoller<\/code> and adds events to it when <code>ReadSome<\/code>, <code>WriteSome<\/code>, <code>Connect<\/code>, etc., are called.<\/p>\n<figure class=\"bordered full-width\"><img loading=\"lazy\" decoding=\"async\" src=\"https:\/\/habrastorage.org\/r\/w1560\/getpro\/habr\/upload_files\/e40\/b58\/d04\/e40b58d04ad89b4d68086e6f5a024ab5.png\" alt=\"class diagram\" title=\"class diagram\" width=\"531\" height=\"671\" data-src=\"https:\/\/habrastorage.org\/getpro\/habr\/upload_files\/e40\/b58\/d04\/e40b58d04ad89b4d68086e6f5a024ab5.png\"\/><\/p>\n<div><figcaption>class diagram<\/figcaption><\/div>\n<\/figure>\n<p>Before moving on to the description of the main components, let&#8217;s take a look at the coroutine API in C++20.<\/p>\n<p><a class=\"anchor\" name=\"3\" id=\"3\"><\/a><\/p>\n<h3>Objects promise and coroutine_handle<\/h3>\n<p>Each coroutine is associated with two entities: <code>promise<\/code> and <code>coroutine_handle<\/code>.<br \/> <code>coroutine_handle<\/code> is necessary for managing the coroutine externally; for<br \/> example, it can be used to resume (<code>resume<\/code>) the coroutine, destroy (<code>destroy<\/code>)<br \/> it, or check if the coroutine is done (<code>done<\/code>). With the <code>promise<\/code> object, you<br \/> can define the behavior of the coroutine at the start and end of its<br \/> execution and manage the coroutine from within. It&#8217;s important to note that the<br \/> methods of the <code>promise<\/code> object should not be called directly; instead, the<br \/> <code>co_return<\/code> and <code>co_yield<\/code> operators are used, and the compiler substitutes the<br \/> appropriate methods.<\/p>\n<p>In the example, the following pair of <code>promise<\/code>\/<code>coroutine_handle<\/code> is used<br \/> (<code>TVoidPromise<\/code>\/<code>TSimpleTask<\/code>):<\/p>\n<pre><code class=\"cpp\">struct TVoidPromise;  struct TSimpleTask : std::coroutine_handle&lt;TVoidPromise> {     using promise_type = TVoidPromise; };  struct TVoidPromise {     TSimpleTask get_return_object() {        return { TSimpleTask::from_promise(*this) };      }     std::suspend_never initial_suspend() { return {}; }     std::suspend_never final_suspend() noexcept { return {}; }     void return_void() {}     void unhandled_exception() {} }; <\/code><\/pre>\n<p>Here, the coroutine doesn&#8217;t return anything (<code>return_void<\/code>), starts<br \/> immediately (<code>std::suspend_never initial_suspend<\/code>), and does not sleep upon<br \/> completion (<code>std::suspend_never final_suspend<\/code>).<\/p>\n<p>In some cases, such as when implementing unit tests, it is convenient to sleep<br \/> the coroutine upon completion:<\/p>\n<pre><code class=\"cpp\">struct TTestPromise;  struct TTestTask : std::coroutine_handle&lt;TTestPromise> {     using promise_type = TTestPromise; };  struct TTestPromise {     TTestTask get_return_object() {        return { TTestTask::from_promise(*this) };      }     std::suspend_never initial_suspend() { return {}; }     std::suspend_always \/* the difference here *\/ final_suspend() noexcept      { return {}; }     void return_void() {}     void unhandled_exception() {} }; <\/code><\/pre>\n<p>Please note that in this case, the <code>promise<\/code> differs only in the signature of<br \/> the <code>final_suspend<\/code> method.<\/p>\n<p><a class=\"anchor\" name=\"4\" id=\"4\"><\/a><\/p>\n<h3>Using final_suspend in Unit Tests<\/h3>\n<p>Let&#8217;s consider a unit test that launches client and server coroutines and then<br \/> checks the correctness of the message exchange:<\/p>\n<pre><code class=\"cpp\">void test_write_after_connect(void**) {     TLoop loop;     TSocket socket(TAddress{\"127.0.0.1\", 8898}, loop.Poller());     socket.Bind();     socket.Listen();     char send_buf[128] = \"Hello\";     char rcv_buf[128] = {0};      TTestTask h1 = [](TLoop* loop, char* buf, int size) -> TTestTask     {         TSocket client(TAddress{\"127.0.0.1\", 8898}, loop->Poller());         co_await client.Connect();         co_await client.WriteSome(buf, size);         co_return;     }(&amp;loop, send_buf, sizeof(send_buf));      TTestTask h2 = [](TSocket* socket, char* buf, int size) -> TTestTask     {         TSocket clientSocket = std::move(co_await socket->Accept());         co_await clientSocket.ReadSome(buf, size);         co_return;     }(&amp;socket, rcv_buf, sizeof(rcv_buf));      while (!(h1.done() &amp;&amp; h2.done())) {         loop.Step();     }     h1.destroy(); h2.destroy();      assert_true(memcmp(&amp;send_buf, &amp;rcv_buf, sizeof(send_buf))==0); } <\/code><\/pre>\n<p>In this case, the event handling loop runs until both coroutines have<br \/> completed. The <code>done<\/code> method checks whether the coroutine has finished.<br \/> However, this method can only be called if the coroutine is &#171;asleep,&#187; which is<br \/> why the <code>promise<\/code> with the <code>std::suspend_always final_suspend<\/code> signature is used<br \/> here.<\/p>\n<p>Before moving on to the implementation of the multiplexer and asynchronous<br \/> socket operations, let&#8217;s describe the main classes, structures, and functions<br \/> used in the program.<\/p>\n<h3>Basic Structures, Classes, and Functions<\/h3>\n<p>For convenience, the following <code>using<\/code> statements are employed for <code>std::chrono<\/code><br \/> (clock-related operations) and <code>std::coroutine_handle&lt;><\/code> objects:<\/p>\n<pre><code class=\"cpp\">using TClock = std::chrono::steady_clock; using TTime = TClock::time_point; using THandle = std::coroutine_handle&lt;>; <\/code><\/pre>\n<p>The low-level structure for working with the network address <code>sockaddr_in<\/code> is encapsulated within the following class:<\/p>\n<pre><code class=\"cpp\">class TAddress { public:     TAddress(const std::string&amp; addr, int port);     TAddress(sockaddr_in addr); }; <\/code><\/pre>\n<p>The POSIX API uses the <code>timeval<\/code> structure to specify a timeout. We need a<br \/> function that converts a time interval specified by two time values into the<br \/> <code>timeval<\/code> structure:<\/p>\n<pre><code class=\"cpp\">timeval GetTimeval(TTime now, TTime deadline); <\/code><\/pre>\n<p>Implementations of <code>TAddress<\/code> and <code>GetTimeval<\/code> are straightforward, and I do<br \/> not provide them here. The code can be viewed <a href=\"https:\/\/github.com\/resetius\/coroio\" rel=\"noopener noreferrer nofollow\">here<\/a>.<\/p>\n<p>To work with timers and socket events, the <code>TTimer<\/code> and <code>TEvent<\/code> structures are used:<\/p>\n<pre><code class=\"cpp\">struct TTimer {     TTime Deadline;     int Fd;     THandle Handle;     bool operator&lt;(const TTimer&amp; e) const {         return std::tuple(Deadline, Fd, !static_cast&lt;bool>(Handle))              &lt; std::tuple(e.Deadline, e.Fd, !static_cast&lt;bool>(e.Handle));     } };  struct TEvent {     int Fd;     enum {         READ = 1,         WRITE = 2     };     int Type;     THandle Handle;      bool Match(const TEvent&amp; other) const {         return Fd == other.Fd &amp;&amp; (Type &amp; other.Type);     } }; <\/code><\/pre>\n<p><code>TTimer<\/code> will be used to implement timeouts on the socket (e.g., connection<br \/> timeout) and for the implementation of the asynchronous <code>Sleep<\/code> function.<br \/> Objects of type <code>TTimer<\/code> will be stored in a priority queue ordered by<br \/> <code>Deadline<\/code>. When the <code>Deadline<\/code> arrives, the <code>Handle<\/code> coroutine is<br \/> awakened. Also, note the comparison function\u2014timers with the same<br \/> <code>Deadline<\/code> and <code>Fd<\/code>, but with an empty <code>Handle<\/code> field, come before<br \/> timers with a filled <code>Handle<\/code> field. This is needed for timer removal.<br \/> Instead of deleting, we will insert a timer with an empty <code>Handle<\/code> into the<br \/> priority queue. During the priority queue processing, we will use this to skip<br \/> removed timers.<\/p>\n<p><code>TEvent<\/code> describes an event on the socket. The event type is specified by<br \/> the <code>Type<\/code> field. An event can be either a read event, a write event, or<br \/> both. When the event occurs, the <code>Handle<\/code> coroutine is awakened.<\/p>\n<p>Using these structures, we will implement an event multiplexer based on <code>select<\/code>.<\/p>\n<p><a class=\"anchor\" name=\"5\" id=\"5\"><\/a><\/p>\n<h3>Event Handling Using select<\/h3>\n<p>First, let&#8217;s describe the class fields. For each socket, we need to store<br \/> <code>THandle<\/code>, which is awakened on read\/write. We will store them in the<br \/> <code>THandlePair<\/code> structure, which consists of the pair <code>THandle Read<\/code> \/<br \/> <code>THandle Write<\/code>. We will also store a priority queue for handling timeouts.<br \/> This queue stores structures of type <code>TTimer<\/code>. After <code>select<\/code> wakes up,<br \/> some coroutines need to be awakened, and for some, we need to send them back to<br \/> <code>select<\/code>. For these purposes, we will maintain two vectors: <code>Changes_<\/code><br \/> and <code>ReadyEvents_<\/code>. The class also stores a pair of utility fields used in<br \/> the <code>select<\/code> call.<\/p>\n<pre><code class=\"cpp\">class TSelect { private:     int MaxFd_ = 0; \/\/ The maximum file descriptor number passed to select     \/\/ Events that have changed (coroutine awakened for them)      \/\/  and need to be sent back to select     std::vector&lt;TEvent> Changes_;      \/\/ Events for which a coroutine needs to be awakened     std::vector&lt;TEvent> ReadyEvents_;      std::priority_queue&lt;TTimer> Timers_;     TTime LastTimersProcessTime_; \/\/ The time when timers were last processed      std::vector&lt;THandlePair> InEvents_; \/\/ Read\/write events sent to select     std::vector&lt;fd_mask> ReadFds_; \/\/ Auxiliary array for select     std::vector&lt;fd_mask> WriteFds_; \/\/ Auxiliary array for select <\/code><\/pre>\n<p>Now let&#8217;s look at the method that prepares arguments for the <code>select<\/code> call,<br \/> invokes <code>select<\/code>, and populates the vector with coroutines to be awakened.<\/p>\n<pre><code class=\"cpp\">    fd_set* ReadFds() {         return reinterpret_cast&lt;fd_set*>(&amp;ReadFds_[0]);     }     fd_set* WriteFds() {         return reinterpret_cast&lt;fd_set*>(&amp;WriteFds_[0]);     }  public:     void Poll() {         auto deadline = Timers_.empty() ? TTime::max() : Timers_.top().Deadline;         auto tv = GetTimeval(TClock::now(), deadline);          constexpr int bits = sizeof(fd_mask)*8;          if (InEvents_.size() &lt;= MaxFd_) {             InEvents_.resize(MaxFd_+1);         }         if (MaxFd_ >= ReadFds_.size()*bits) {             ReadFds_.resize((MaxFd_+bits)\/bits);             WriteFds_.resize((MaxFd_+bits)\/bits);         }          for (const auto&amp; ch : Changes_) {             int fd = ch.Fd;             auto&amp; ev = InEvents_[fd];             if (ch.Handle) {                 if (ch.Type &amp; TEvent::READ) {                     FD_SET(fd, ReadFds()); ev.Read = ch.Handle;                 }                 if (ch.Type &amp; TEvent::WRITE) {                     FD_SET(fd, WriteFds()); ev.Write = ch.Handle;                 }             } else {                 if (ch.Type &amp; TEvent::READ) {                     FD_CLR(fd, ReadFds()); ev.Read = {};                 }                 if (ch.Type &amp; TEvent::WRITE) {                     FD_CLR(fd, WriteFds()); ev.Write = {};                 }             }         }          ReadyEvents_.clear(); Changes_.clear(); MaxFd_ = 0;          if (select(InEvents_.size(), ReadFds(), WriteFds(), nullptr, &amp;tv) &lt; 0) {             throw std::system_error(errno, std::generic_category(), \"select\");         }  <\/code><\/pre>\n<p>Reading\/writing events on the socket are indicated in <code>select<\/code> by setting bits<br \/> in the bit arrays <code>ReadFds_<\/code> and <code>WriteFds_<\/code>. Therefore, we must iterate<br \/> through all the changed events and set or clear bits for them in <code>ReadFds_<\/code> and<br \/> <code>WriteFds_<\/code>. The occurrence of events like <code>Timeout<\/code> is determined by setting<br \/> the last argument of <code>select<\/code>. The <code>tv<\/code> argument is set so that <code>select<\/code> wakes<br \/> up when the <code>Deadline<\/code> of the first <code>TTimer<\/code> in the priority queue is reached.<\/p>\n<p>After waking up from <code>select<\/code>, we iterate through the array of events and<br \/> check the read\/write bit in the bit arrays <code>ReadFds_<\/code> and <code>WriteFds_<\/code>. If the<br \/> corresponding bit is set, we place the event in the <code>ReadyEvents_<\/code> array. We<br \/> will traverse this array later and awaken all the coroutines in it.<\/p>\n<pre><code class=\"cpp\">        for (int k=0; k &lt; static_cast&lt;int>(InEvents_.size()); ++k) {             auto ev = InEvents_[k];              if (FD_ISSET(k, WriteFds())) {                 assert(ev.Write);                 ReadyEvents_.emplace_back(TEvent{k, TEvent::WRITE, ev.Write});             } else if (ev.Write) {                 \/\/ fd was cleared by select, set it                 FD_SET(k, WriteFds());             }             if (FD_ISSET(k, ReadFds())) {                 assert(ev.Read);                 ReadyEvents_.emplace_back(TEvent{k, TEvent::READ, ev.Read});             } else if (ev.Read) {                 \/\/ fd was cleared by select, set it                 FD_SET(k, ReadFds());             }         }          ProcessTimers();     } <\/code><\/pre>\n<p>Now let&#8217;s consider the processing of timers:<\/p>\n<pre><code class=\"cpp\">    void ProcessTimers() {         auto now = TClock::now();         int prevFd = -1;         while (!Timers_.empty()&amp;&amp;Timers_.top().Deadline &lt;= now) {             TTimer timer = std::move(Timers_.top());              if ((prevFd == -1 || prevFd != timer.Fd) &amp;&amp; timer.Handle) {                 ReadyEvents_.emplace_back(TEvent{-1, 0, timer.Handle});             }              prevFd = timer.Fd;             Timers_.pop();         }         LastTimersProcessTime_ = now;     } <\/code><\/pre>\n<p>We iterate through the priority queue, and if the <code>Deadline<\/code> has arrived, we<br \/> place the event in <code>ReadyEvents_<\/code>. It&#8217;s important to consider that the timer<br \/> might have been removed; in this case, we need to skip it and not place it in<br \/> <code>ReadyEvents_<\/code>.<\/p>\n<p>Adding read\/write events and timers is done trivially:<\/p>\n<pre><code class=\"cpp\">    void AddTimer(int fd, TTime deadline, THandle h) {         Timers_.emplace(TTimer{deadline, fd, h});     }      void AddRead(int fd, THandle h) {         MaxFd_ = std::max(MaxFd_, fd);         Changes_.emplace_back(TEvent{fd, TEvent::READ, h});     }      void AddWrite(int fd, THandle h) {         MaxFd_ = std::max(MaxFd_, fd);         Changes_.emplace_back(TEvent{fd, TEvent::WRITE, h});     } <\/code><\/pre>\n<p>Removing an event is accomplished by inserting an empty event into <code>Changes_<\/code>:<\/p>\n<pre><code class=\"cpp\">    void RemoveEvent(int fd) {         MaxFd_ = std::max(MaxFd_, fd);         Changes_.emplace_back(TEvent{fd, TEvent::READ|TEvent::WRITE, {}});     } <\/code><\/pre>\n<p>Removing a timer is done by inserting an empty timer into <code>Timers_<\/code>:<\/p>\n<pre><code class=\"cpp\">    bool RemoveTimer(int fd, TTime deadline) {         bool fired = deadline &lt; LastTimersProcessTime_;         if (!fired) {             Timers_.emplace(TTimer{deadline, fd, {}});         }         return fired;     } <\/code><\/pre>\n<p>Now let&#8217;s consider the awakening of coroutines:<\/p>\n<pre><code class=\"cpp\">    void Wakeup(TEvent&amp;&amp; change) {         change.Handle.resume();         if (Changes_.empty() || !Changes_.back().Match(change)) {             if (change.Fd >= 0) {                 change.Handle = {};                 Changes_.emplace_back(std::move(change));             }         }     }      void WakeupReadyHandles() {         int i = 0;         for (auto&amp;&amp; ev : ReadyEvents_) {             Wakeup(std::move(ev));         }     } }; <\/code><\/pre>\n<p>The <code>Wakeup<\/code> method awakens a coroutine. After awakening and execution, the<br \/> coroutine might fall asleep again, waiting for the same descriptor on which we<br \/> previously woke up, or it might fall asleep on another descriptor. If the<br \/> coroutine falls asleep on a different descriptor, the one that triggered the<br \/> awakening must be removed from <code>select<\/code>. This check and removal are performed<br \/> in the condition <code>if (Changes_.empty() ...)<\/code>. The <code>WakeupReadyHandles<\/code> method<br \/> traverses all events in <code>ReadyEvents_<\/code> and awakens coroutines.<\/p>\n<p>That concludes the description of the <code>TSelect<\/code> class. The <code>Poll<\/code> and<br \/> <code>WakeupReadyHandles<\/code> methods will be used in the event handling loop, and the<br \/> other methods will be used by the socket class in read\/write\/connection<br \/> methods.<\/p>\n<p>The event handling loop is implemented trivially:<\/p>\n<pre><code class=\"cpp\">template&lt;typename TPoller> class TLoop { public:     void Loop() {         while (Running_) {             Step();         }     }      void Stop() {         Running_ = false;     }      void Step() {         Poller_.Poll();         Poller_.WakeupReadyHandles();     }      TPoller&amp; Poller() {         return Poller_;     }  private:     TPoller Poller_;     bool Running_ = true; }; <\/code><\/pre>\n<p>This is a template class parameterized by the type <code>TPoller<\/code>, which can be<br \/> <code>TSelect<\/code>. This is done to allow for swapping out the implementation of the<br \/> multiplexer (using <code>epoll<\/code> instead of <code>select<\/code>, etc.). Also, for convenience<br \/> and future code extension, let&#8217;s place all methods and fields needed by the<br \/> socket for operation in the base class <code>TPollerBase<\/code> and inherit <code>TSelect<\/code><br \/> from <code>TPollerBase<\/code>. Thus, <code>TPollerBase<\/code> will contain methods <code>AddTimer<\/code>,<br \/> <code>RemoveTimer<\/code>, <code>AddRead<\/code>, <code>AddWrite<\/code>, <code>RemoveEvent<\/code>. The code for this<br \/> enhancement is trivial. Later, the socket class will work with the base class.<\/p>\n<h3>Socket<\/h3>\n<p>As fields, the socket has a descriptor <code>Fd_<\/code>, a pointer to the base class<br \/> poller <code>Poller_<\/code>, and an address <code>Addr_<\/code>. Constructors and destructors are<br \/> trivial. In the constructor, a socket is created and set to non-blocking mode<br \/> using <code>fcntl<\/code>. In the destructor, the descriptor is closed with <code>close<\/code>, and<br \/> events associated with the socket are removed from <code>Poller_<\/code>.<\/p>\n<pre><code class=\"cpp\">class TSocket {     int Fd_ = -1;     TPollerBase* Poller_ = nullptr;     TAddress Addr_;  public:     \/\/ Obvious things: Constructors, assignment operators, destructors <\/code><\/pre>\n<p>Let&#8217;s present the code for the main socket methods &#8212; establishing a<br \/> connection, accepting a connection, reading, and writing. All these methods<br \/> will return a structure called <code>TAwaitable<\/code>, containing the methods<br \/> <code>await_ready<\/code>, <code>await_suspend<\/code>, <code>await_resume<\/code>. The compiler will insert<br \/> calls to these methods into the code. First, the <code>await_ready<\/code> method is<br \/> called; if it returns <code>true<\/code>, the coroutine will not go into a sleeping<br \/> state, and instead, the <code>await_resume<\/code> method will be immediately called to<br \/> get the result. If the <code>await_ready<\/code> method returns <code>false<\/code>, the<br \/> <code>await_suspend<\/code> method is called, and a reference to the coroutine is passed<br \/> as an argument. This reference can be saved and later used to awaken the<br \/> coroutine. Upon awakening, the <code>await_resume<\/code> method is always called,<br \/> returning the result.<\/p>\n<p>Let&#8217;s examine the <code>Connect<\/code> method, which establishes a connection. Here,<br \/> in <code>await_ready<\/code>, we attempt to establish a connection in non-blocking mode,<br \/> and if successful, we return <code>true<\/code>. If it&#8217;s not possible to do it<br \/> immediately, we return <code>false<\/code> and go to sleep. We wake up when <code>select<\/code><br \/> indicates that the connection is established and operations can be performed<br \/> with the socket (in this case, write operations). Note that here, with the<br \/> help of a timer (<code>AddTimer<\/code>) and the <code>deadline<\/code> parameter, you can set the<br \/> maximum connection waiting time.<\/p>\n<pre><code class=\"cpp\">auto Connect(TTime deadline = TTime::max()) {     struct TAwaitable {         bool await_ready() {             int ret = connect(fd, (struct sockaddr*) &amp;addr, sizeof(addr));             if (ret &lt; 0                  &amp;&amp; !(errno == EINTR||errno==EAGAIN||errno==EINPROGRESS)) {                 throw std::system_error(errno,                                          std::generic_category(), \"connect\");             }             return ret >= 0;         }          void await_suspend(std::coroutine_handle&lt;> h) {             poller->AddWrite(fd, h);             if (deadline != TTime::max()) {                 poller->AddTimer(fd, deadline, h);             }         }          void await_resume() {             if (deadline != TTime::max()                  &amp;&amp; poller->RemoveTimer(fd, deadline)) {                 throw std::system_error(                   std::make_error_code(std::errc::timed_out));             }         }          TPollerBase* poller;         int fd;         sockaddr_in addr;         TTime deadline;     };     return TAwaitable{Poller_, Fd_, Addr_.Addr(), deadline}; } <\/code><\/pre>\n<p>The read method works similarly, but in this case, we try to read data using<br \/> <code>read<\/code>. If it fails, we put the coroutine to sleep, and the descriptor is<br \/> sent to wait in <code>select<\/code>.<\/p>\n<pre><code class=\"cpp\">auto ReadSome(char* buf, size_t size) {     struct TAwaitable  {         bool await_ready() {             Run();             return ready = (ret >= 0);         }          int await_resume() {             if (!ready) { Run(); }             return ret;         }          void Run() {             ret = read(fd, b, s);             if (ret &lt; 0 &amp;&amp; !(err == EINTR||err==EAGAIN||err==EINPROGRESS)) {                 throw std::system_error(errno,                                          std::generic_category(), \"read\");             }         }          void await_suspend(std::coroutine_handle&lt;> h) {             poller->AddRead(fd, h);         }          TSelect* poller;         int fd;         char* b; size_t s;         int ret;         bool ready;     };     return TAwaitable{Poller_,Fd_,buf,size}; } <\/code><\/pre>\n<p>Let&#8217;s add another method, <code>ReadSomeYield<\/code>, which will always put the coroutine<br \/> to sleep immediately and send the descriptor to wait in <code>select<\/code>. This method<br \/> is useful for tests and benchmarks.<\/p>\n<pre><code class=\"cpp\">    auto ReadSomeYield(char* buf, size_t size) {         struct TAwaitable  {             bool await_ready() {                 return (ready = false);             }             ...             same code here             ...         };         return TAwaitable{Poller_,Fd_,buf,size};     } <\/code><\/pre>\n<p>The write methods <code>WriteSome<\/code> and <code>WriteSomeYield<\/code> are implemented similarly,<br \/> but instead of calling <code>read<\/code>, <code>write<\/code> is used everywhere. In case of<br \/> sleeping, an event of type Write is added to the poller using <code>AddWrite<\/code>.<\/p>\n<pre><code class=\"cpp\">    auto WriteSome(char* buf, size_t size) {         \/\/ Same as ReadSome     }      auto WriteSomeYield(char* buf, size_t size) {         \/\/ Same as ReadSomeYield     } <\/code><\/pre>\n<p>The method for accepting connections, <code>Accept<\/code>, used by the server, is<br \/> implemented as follows.<\/p>\n<pre><code class=\"cpp\">auto Accept() {     struct TAwaitable {         bool await_ready() const { return false; }         void await_suspend(std::coroutine_handle&lt;> h) {             poller->AddRead(fd, h);         }         TSocket await_resume() {             sockaddr_in clientaddr;             socklen_t len = sizeof(clientaddr);              int clientfd = accept(fd, (sockaddr*)&amp;clientaddr, &amp;len);             if (clientfd &lt; 0) {                 throw std::system_error(errno,                                          std::generic_category(), \"accept\");             }              return TSocket{clientaddr, clientfd, *poller};         }          TSelect* poller;         int fd;     };      return TAwaitable{Poller_, Fd_}; } <\/code><\/pre>\n<p>In <code>await_ready<\/code>, we go to sleep immediately and wait for <code>select<\/code> to respond<br \/> that the server socket is readable (<code>AddRead<\/code>). If this happens, it means a<br \/> new client has arrived. In this case, we obtain the client socket using<br \/> the <code>accept<\/code> call, wrap it in <code>TSocket<\/code>, and return the result.<\/p>\n<p>There are also the <code>Bind<\/code> and <code>Listen<\/code> methods, which are trivial wrappers<br \/> around the <code>bind<\/code> and <code>listen<\/code> calls. The first method binds the socket to an<br \/> address, and the second method puts the socket in passive mode, allowing the<br \/> <code>accept<\/code> method to be called to accept connections.<\/p>\n<pre><code class=\"cpp\">    \/* assign the socket the         required address to which clients will send requests *\/     void Bind() { \/* trivial *\/ }      void Listen() { \/* trivial *\/ } }; <\/code><\/pre>\n<hr\/>\n<p><a class=\"anchor\" name=\"6\" id=\"6\"><\/a><\/p>\n<h3>Performance<\/h3>\n<p>The full code of the examples discussed above can be downloaded using the <a href=\"https:\/\/github.com\/resetius\/coroio\" rel=\"noopener noreferrer nofollow\">link<\/a>.<br \/> The complete version includes support not only for <code>select<\/code> but also for <code>poll<\/code>, <code>epoll<\/code>, <code>kqueue<\/code>, and even <code>uring<\/code>.<\/p>\n<p>Our solution has proven its effectiveness in terms of usability, as seen in<br \/> the client and server code. Now let&#8217;s look at the performance of the<br \/> written library and compare it with a similar solution that uses callbacks<br \/> for code implementation.<\/p>\n<p>We&#8217;ll apply the methodology from <a href=\"https:\/\/libevent.org\/\" rel=\"noopener noreferrer nofollow\">libevent<\/a>. Create N pipes that pass<br \/> data to each other as follows:<\/p>\n<pre><code class=\"cpp\">struct Stat {     int writes = 0;     int fired = 0;     int count = 0;     int failures = 0;     int out = 0; };  template&lt;typename TSocket> TTestTask pipe_reader(TSocket&amp; r, TSocket&amp; w, Stat&amp; s) {     ssize_t size;     char buf[1] = {0};      try {         while ((size = co_await r.ReadSomeYield(buf, 1)) != 0) {             s.count += size;             if (s.writes) {                 s.writes--;                 if (co_await w.WriteSome(buf, 1) != 1) {                     s.failures ++;                 }                 s.fired++;             }         }     } catch (const std::exception&amp; ex) {         s.failures ++;     }     s.out ++;     co_return; } <\/code><\/pre>\n<p>We read data from one pipe and write it to the next one in the chain. We<br \/> collect statistics about the number of bytes read, the number of <code>write<\/code> calls,<br \/> the number of remaining <code>write<\/code> calls, and so on in the <code>Stat<\/code> structure.<br \/> A similar benchmark included in libevent also collects similar statistics. To<br \/> ensure that the coroutine benchmark operates exactly like the callback-based<br \/> libevent benchmark, I compared the counter values of the statistics after<br \/> running the benchmark. The counter values precisely matched the values<br \/> provided by the libevent benchmark.<\/p>\n<p>We write data into M pipes as follows:<\/p>\n<pre><code class=\"cpp\">template&lt;typename TSocket> TTestTask write_one(TSocket&amp; w, Stat&amp; s) {     char buf[1] = {'e'};     co_await w.WriteSome(buf, 1);     s.fired ++;     co_return; } <\/code><\/pre>\n<p>Let&#8217;s run two benchmarks. In the first one, we initially write data into one<br \/> pipe with M=1, meaning one pipe is active while the others are passive. We<br \/> increase the total number of pipes and observe how the processing time for all<br \/> events changes as we increase the number of pipes. In the second benchmark, we<br \/> set M=100. The results for various operating systems and CPUs are provided<br \/> below.<br \/> As you can see, the developed solution performs as well as libevent, and for<br \/> backend <code>select<\/code> and <code>poll<\/code>, it even performs better.<\/p>\n<p>Below are the results of two benchmarks for the i7-12800H and i5-11400F processors, as well as the Apple M1. Results for the libevent library are also provided for comparison.    <\/p>\n<figure class=\"full-width\"><img loading=\"lazy\" decoding=\"async\" src=\"https:\/\/habrastorage.org\/r\/w1560\/getpro\/habr\/upload_files\/677\/a6b\/a27\/677a6ba2711a0f1a3f3913a450ec1081.png\" alt=\"CPU i7-12800H, Ubuntu 23.04, clang 16\" title=\"CPU i7-12800H, Ubuntu 23.04, clang 16\" width=\"800\" height=\"600\" data-src=\"https:\/\/habrastorage.org\/getpro\/habr\/upload_files\/677\/a6b\/a27\/677a6ba2711a0f1a3f3913a450ec1081.png\"\/><\/p>\n<div><figcaption>CPU i7-12800H, Ubuntu 23.04, clang 16<\/figcaption><\/div>\n<\/figure>\n<figure class=\"full-width\"><img loading=\"lazy\" decoding=\"async\" src=\"https:\/\/habrastorage.org\/r\/w1560\/getpro\/habr\/upload_files\/3dd\/e66\/810\/3dde668108599b8d9d5e083daad2e5ab.png\" alt=\"CPU i7-12800H, Ubuntu 23.04, clang 16\" title=\"CPU i7-12800H, Ubuntu 23.04, clang 16\" width=\"800\" height=\"600\" data-src=\"https:\/\/habrastorage.org\/getpro\/habr\/upload_files\/3dd\/e66\/810\/3dde668108599b8d9d5e083daad2e5ab.png\"\/><\/p>\n<div><figcaption>CPU i7-12800H, Ubuntu 23.04, clang 16<\/figcaption><\/div>\n<\/figure>\n<figure class=\"full-width\"><img loading=\"lazy\" decoding=\"async\" src=\"https:\/\/habrastorage.org\/r\/w1560\/getpro\/habr\/upload_files\/a8f\/ee4\/d49\/a8fee4d4933e8e2624a15f8e32f377a3.png\" alt=\"CPU i5-11400F, Ubuntu 23.04, WSL2, kernel 6.1.21.1-microsoft-standard-WSL2+\" title=\"CPU i5-11400F, Ubuntu 23.04, WSL2, kernel 6.1.21.1-microsoft-standard-WSL2+\" width=\"800\" height=\"600\" data-src=\"https:\/\/habrastorage.org\/getpro\/habr\/upload_files\/a8f\/ee4\/d49\/a8fee4d4933e8e2624a15f8e32f377a3.png\"\/><\/p>\n<div><figcaption>CPU i5-11400F, Ubuntu 23.04, WSL2, kernel 6.1.21.1-microsoft-standard-WSL2+<\/figcaption><\/div>\n<\/figure>\n<figure class=\"full-width\"><img loading=\"lazy\" decoding=\"async\" src=\"https:\/\/habrastorage.org\/r\/w1560\/getpro\/habr\/upload_files\/6fb\/9b2\/be7\/6fb9b2be78d41c180f0f3a569b100683.png\" alt=\"CPU i5-11400F, Ubuntu 23.04, WSL2, kernel 6.1.21.1-microsoft-standard-WSL2+\" title=\"CPU i5-11400F, Ubuntu 23.04, WSL2, kernel 6.1.21.1-microsoft-standard-WSL2+\" width=\"800\" height=\"600\" data-src=\"https:\/\/habrastorage.org\/getpro\/habr\/upload_files\/6fb\/9b2\/be7\/6fb9b2be78d41c180f0f3a569b100683.png\"\/><\/p>\n<div><figcaption>CPU i5-11400F, Ubuntu 23.04, WSL2, kernel 6.1.21.1-microsoft-standard-WSL2+<\/figcaption><\/div>\n<\/figure>\n<figure class=\"full-width\"><img loading=\"lazy\" decoding=\"async\" src=\"https:\/\/habrastorage.org\/r\/w1560\/getpro\/habr\/upload_files\/b67\/564\/7a7\/b675647a7e9cde8d3e5cac8fcf3b6468.png\" alt=\"CPU Apple M1, MacBook Air M1 16G, MacOS 12.6.3\" title=\"CPU Apple M1, MacBook Air M1 16G, MacOS 12.6.3\" width=\"800\" height=\"600\" data-src=\"https:\/\/habrastorage.org\/getpro\/habr\/upload_files\/b67\/564\/7a7\/b675647a7e9cde8d3e5cac8fcf3b6468.png\"\/><\/p>\n<div><figcaption>CPU Apple M1, MacBook Air M1 16G, MacOS 12.6.3<\/figcaption><\/div>\n<\/figure>\n<figure class=\"full-width\"><img loading=\"lazy\" decoding=\"async\" src=\"https:\/\/habrastorage.org\/r\/w1560\/getpro\/habr\/upload_files\/cff\/c87\/fe7\/cffc87fe750b587f5086ae05657258e0.png\" alt=\"CPU Apple M1, MacBook Air M1 16G, MacOS 12.6.3\" title=\"CPU Apple M1, MacBook Air M1 16G, MacOS 12.6.3\" width=\"800\" height=\"600\" data-src=\"https:\/\/habrastorage.org\/getpro\/habr\/upload_files\/cff\/c87\/fe7\/cffc87fe750b587f5086ae05657258e0.png\"\/><\/p>\n<div><figcaption>CPU Apple M1, MacBook Air M1 16G, MacOS 12.6.3<\/figcaption><\/div>\n<\/figure>\n<h3>Conclusion<\/h3>\n<p>We have successfully developed a high-performance network library that not only excels in efficiency but also offers ease of use and extendability. The library&#8217;s unique design supports asynchronous I\/O for both sockets and files by leveraging the capabilities of the uring backend. The code snippets provided are straightforward, mirroring the structure of typical synchronous code, thereby simplifying the learning curve for new users. This combination of performance, simplicity, and extendability makes our library a robust tool for building efficient, scalable network applications. It stands as a testament to the power and flexibility of modern C++ programming using coroutines.<\/p>\n<h3>Links<\/h3>\n<ol>\n<li>\n<p><a href=\"https:\/\/en.wikipedia.org\/wiki\/Callback_(computer_programming)\" rel=\"noopener noreferrer nofollow\">Callbacks<\/a><\/p>\n<\/li>\n<li>\n<p><a href=\"https:\/\/en.wikipedia.org\/wiki\/Actor_model\" rel=\"noopener noreferrer nofollow\">Actors<\/a><\/p>\n<\/li>\n<li>\n<p><a href=\"https:\/\/en.wikipedia.org\/wiki\/Futures_and_promises\" rel=\"noopener noreferrer nofollow\">Future\/Promise<\/a><\/p>\n<\/li>\n<li>\n<p><a href=\"https:\/\/en.wikipedia.org\/wiki\/Coroutine\" rel=\"noopener noreferrer nofollow\">Coroutines<\/a><\/p>\n<\/li>\n<li>\n<p><a href=\"https:\/\/en.cppreference.com\/w\/cpp\/language\/coroutines\" rel=\"noopener noreferrer nofollow\">Coroutines in C++<\/a><\/p>\n<\/li>\n<li>\n<p><a href=\"https:\/\/libevent.org\/\" rel=\"noopener noreferrer nofollow\">Libevent<\/a><\/p>\n<\/li>\n<li>\n<p><a href=\"https:\/\/github.com\/resetius\/coroio\" rel=\"noopener noreferrer nofollow\">resetius\/coroio<\/a><\/p>\n<\/li>\n<\/ol>\n<\/div>\n<\/div>\n<\/div>\n<p><!----><!----><\/div>\n<p><!----><!----><br \/> \u0441\u0441\u044b\u043b\u043a\u0430 \u043d\u0430 \u043e\u0440\u0438\u0433\u0438\u043d\u0430\u043b \u0441\u0442\u0430\u0442\u044c\u0438 <a href=\"https:\/\/habr.com\/ru\/articles\/768418\/\"> https:\/\/habr.com\/ru\/articles\/768418\/<\/a><\/p>\n","protected":false},"excerpt":{"rendered":"<div><!--[--><!--]--><\/div>\n<div id=\"post-content-body\">\n<div>\n<div class=\"article-formatted-body article-formatted-body article-formatted-body_version-2\">\n<div xmlns=\"http:\/\/www.w3.org\/1999\/xhtml\">\n<figure class=\"full-width\"><\/figure>\n<p>Asynchronous programming is commonly employed for efficient implementation of network interactions in C++. The essence of this approach lies in the fact that the results of socket read\/write functions are not immediately available but become accessible after some time. This approach allows for loading the processor with useful work during the wait for data. Various implementations of this approach exist, such as <a href=\"https:\/\/en.wikipedia.org\/wiki\/Callback_(computer_programming)\" rel=\"noopener noreferrer nofollow\">callbacks<\/a>, <a href=\"https:\/\/en.wikipedia.org\/wiki\/Actor_model\" rel=\"noopener noreferrer nofollow\">actors<\/a>, <a href=\"https:\/\/en.wikipedia.org\/wiki\/Futures_and_promises\" rel=\"noopener noreferrer nofollow\">future\/promise<\/a>, <a href=\"https:\/\/en.wikipedia.org\/wiki\/Coroutine\" rel=\"noopener noreferrer nofollow\">coroutines<\/a>. In C++, these implementations are available as libraries from third-party developers or can be implemented independently.<\/p>\n<p>Coroutines are the most challenging to implement as they require writing platform-dependent code. However, the recent version of the C++ 20 language standard introduces support for coroutines at the compiler and standard library levels. Coroutines are functions that can suspend their execution, preserving their state, and later return to that state to resume the function&#8217;s work. The compiler automatically creates a checkpoint with the coroutine&#8217;s state.<\/p>\n<p>For a comprehensive understanding of C++ 20 coroutines, refer to this article. Below, we examine a code example using coroutines and describe important points applied during implementation.<\/p>\n<p>For a complete understanding and assimilation of the material presented in this article, it is necessary for the reader to possess basic knowledge in the field of sockets and the TCP\/IP protocol. Fundamental concepts, such as mechanisms of interaction through sockets, principles of data transmission via the TCP\/IP protocol, as well as an understanding of key terms in this domain, will be crucial for the successful comprehension of the provided information. GeeksforGeeks offers useful tutorials on <a href=\"https:\/\/www.geeksforgeeks.org\/tcp-server-client-implementation-in-c\/\" rel=\"noopener noreferrer nofollow\">TCP Server-Client Implementation in C++<\/a> and <a href=\"https:\/\/www.geeksforgeeks.org\/socket-programming-cc\/\" rel=\"noopener noreferrer nofollow\">Socket Programming in C\/C++<\/a>, which can serve as valuable resources for enhancing your understanding of these foundational concepts.<\/p>\n<p>The code of the library discussed below, along with usage examples, can be downloaded from the following link: <a href=\"https:\/\/github.com\/resetius\/coroio\" rel=\"noopener noreferrer nofollow\">https:\/\/github.com\/resetius\/coroio<\/a>.<\/p>\n<h3>Contents<\/h3>\n<ol>\n<li>\n<p><a href=\"#1\" rel=\"noopener noreferrer nofollow\">Network Interaction: Echo Client and Server Example<\/a><\/p>\n<\/li>\n<li>\n<p><a href=\"#2\" rel=\"noopener noreferrer nofollow\">Echo Client and Server Code Overview<\/a><\/p>\n<\/li>\n<li>\n<p><a href=\"#3\" rel=\"noopener noreferrer nofollow\">promise and coroutine_handle<\/a><\/p>\n<\/li>\n<li>\n<p><a href=\"#4\" rel=\"noopener noreferrer nofollow\">Using final_suspend in Unit Tests<\/a><\/p>\n<\/li>\n<li>\n<p><a href=\"#5\" rel=\"noopener noreferrer nofollow\">Event Handling Using select<\/a><\/p>\n<\/li>\n<li>\n<p><a href=\"#6\" rel=\"noopener noreferrer nofollow\">Performance and benchmarks<\/a><\/p>\n<\/li>\n<\/ol>\n<hr\/>\n<p><a class=\"anchor\" name=\"1\" id=\"1\"><\/a><\/p>\n<h3>Network Interaction: Echo Client and Server Example<\/h3>\n<p>Let&#8217;s examine a classic example of network interaction\u2014the implementation of an echo client and server. This example illustrates the task of implementing network programs and effectively demonstrates the details of a specific approach used for asynchronous programming. The echo client\/server example is often included in the documentation of third-party libraries.<\/p>\n<p>The echo server receives a message from the client and immediately sends it back unchanged as a response. The echo client sends a message to the server and waits for a response. Here&#8217;s an example of the process.<\/p>\n<p>This is a client session:<\/p>\n<pre><code class=\"bash\">$ .\/echoclient test Received: test  message Received: message  yet another message Received: yet another message <\/code><\/pre>\n<p>This is a server session:<\/p>\n<pre><code class=\"bash\">$ .\/echoserver Received: test  Received: message  Received: yet another message <\/code><\/pre>\n<p><a class=\"anchor\" name=\"2\" id=\"2\"><\/a><\/p>\n<h3>Echo Client and Server Code Overview<\/h3>\n<p>Let&#8217;s examine the Echo client and server code from top to bottom. We&#8217;ll start<br \/> by looking at the overall code structure, then move on to exploring the C++<br \/> coroutine API, and finally, we&#8217;ll descend to the POSIX API level for socket<br \/> operations. It&#8217;s worth noting that no third-party libraries are used in this<br \/> implementation. Only the standard C++ library and the POSIX API are employed.<\/p>\n<p>The Echo client reads from standard input, sends the input to the socket,<br \/> and waits for a response:<\/p>\n<pre><code class=\"cpp\">TSimpleTask client(TLoop* loop) {     char out[128] = {0};     char in[128] = {0};     ssize_t size = 1;      try {         TSocket input{TAddress{}, 0 \/* stdin *\/, loop->Poller()};         TSocket socket{TAddress{\"127.0.0.1\", 8888}, loop->Poller()};          co_await socket.Connect();         while (size &amp;&amp; (size = co_await input.ReadSome(out, sizeof(out)))) {             co_await socket.WriteSome(out, size);             size = co_await socket.ReadSome(in, sizeof(in));             std::cout &lt;&lt; \"Received: \" &lt;&lt; std::string_view(in, size) &lt;&lt; \"\\n\";         }     } catch (const std::exception&amp; ex) {         std::cout &lt;&lt; \"Exception: \" &lt;&lt; ex.what() &lt;&lt; \"\\n\";     }     loop->Stop();     co_return; }  int main(int argc, char** argv) {     TLoop loop;     client(&amp;loop);     loop.Loop();     return 0; } <\/code><\/pre>\n<p>The Echo server listens on a port, accepts incoming connections (calls<br \/> <code>accept<\/code>), and for each connection, a separate coroutine is created. This<br \/> coroutine reads data from the socket and immediately sends the read data back<br \/> to the socket:<\/p>\n<pre><code class=\"cpp\">TSimpleTask client_handler(TSocket socket, TLoop* loop) {     char buffer[128] = {0}; ssize_t size = 0;      try {         while ((size = co_await socket.ReadSome(buffer, sizeof(buffer))) > 0) {             std::cerr &lt;&lt; \"Received: \" &lt;&lt; std::string_view(buffer, size) &lt;&lt; \"\\n\";             co_await socket.WriteSome(buffer, size);         }     } catch (const std::exception&amp; ex) {         std::cerr &lt;&lt; \"Exception: \" &lt;&lt; ex.what() &lt;&lt; \"\\n\";     }     co_return; }  TSimpleTask server(TLoop* loop) {     TSocket socket(TAddress{\"0.0.0.0\", 8888}, loop->Poller());     socket.Bind();     socket.Listen();      while (true) {         auto client = co_await socket.Accept();         client_handler(std::move(client), loop);     }     co_return; }  int main(int argc, char** argv) {     TLoop loop;     server(&amp;loop);     loop.Loop();     return 0; } <\/code><\/pre>\n<p>The client and server share a similar structure: the <code>main<\/code> function starts an<br \/> event handling loop (<code>TLoop<\/code>) and launches a coroutine. The event handling loop<br \/> interacts with an object (<code>TPoller<\/code>), which performs input-output multiplexing.<br \/> In this implementation, input-output multiplexing is achieved using the <code>select<\/code><br \/> mechanism, and changing it to <code>poll<\/code>, <code>epoll<\/code>, <code>kqueue<\/code>, etc., should not pose a<br \/> problem. <code>TPoller<\/code> waits for events on the socket or a timeout, passes these<br \/> events to the <code>TLoop<\/code> loop, which, in turn, wakes up the coroutines subscribed to<br \/> these events.<\/p>\n<p>The following diagram illustrates how the program switches the execution flow between the<br \/> <code>client<\/code> coroutine and the <code>Loop.loop()<\/code> function when the coroutine sleeps on<br \/> each <code>co_await<\/code> operator call (in reality, it may not sleep every time;<br \/> details below).<\/p>\n<figure class=\"\">\n<div><figcaption>coroutine suspend\/resume<\/figcaption><\/div>\n<\/figure>\n<p>Below is a diagram of the main classes implemented in our library. <code>TPoller<\/code> has several implementations (<code>select<\/code>, <code>poll<\/code>, <code>epoll<\/code>, <code>kqueue<\/code>, <code>uring<\/code>). <code>TLoop<\/code> contains <code>TPoller<\/code> &#8212; its responsibility is to initiate the <code>Poll<\/code>. <code>TSocket<\/code> contains a reference to <code>TPoller<\/code> and adds events to it when <code>ReadSome<\/code>, <code>WriteSome<\/code>, <code>Connect<\/code>, etc., are called.<\/p>\n<figure class=\"bordered full-width\">\n<div><figcaption>class diagram<\/figcaption><\/div>\n<\/figure>\n<p>Before moving on to the description of the main components, let&#8217;s take a look at the coroutine API in C++20.<\/p>\n<p><a class=\"anchor\" name=\"3\" id=\"3\"><\/a><\/p>\n<h3>Objects promise and coroutine_handle<\/h3>\n<p>Each coroutine is associated with two entities: <code>promise<\/code> and <code>coroutine_handle<\/code>.<br \/> <code>coroutine_handle<\/code> is necessary for managing the coroutine externally; for<br \/> example, it can be used to resume (<code>resume<\/code>) the coroutine, destroy (<code>destroy<\/code>)<br \/> it, or check if the coroutine is done (<code>done<\/code>). With the <code>promise<\/code> object, you<br \/> can define the behavior of the coroutine at the start and end of its<br \/> execution and manage the coroutine from within. It&#8217;s important to note that the<br \/> methods of the <code>promise<\/code> object should not be called directly; instead, the<br \/> <code>co_return<\/code> and <code>co_yield<\/code> operators are used, and the compiler substitutes the<br \/> appropriate methods.<\/p>\n<p>In the example, the following pair of <code>promise<\/code>\/<code>coroutine_handle<\/code> is used<br \/> (<code>TVoidPromise<\/code>\/<code>TSimpleTask<\/code>):<\/p>\n<pre><code class=\"cpp\">struct TVoidPromise;  struct TSimpleTask : std::coroutine_handle&lt;TVoidPromise> {     using promise_type = TVoidPromise; };  struct TVoidPromise {     TSimpleTask get_return_object() {        return { TSimpleTask::from_promise(*this) };      }     std::suspend_never initial_suspend() { return {}; }     std::suspend_never final_suspend() noexcept { return {}; }     void return_void() {}     void unhandled_exception() {} }; <\/code><\/pre>\n<p>Here, the coroutine doesn&#8217;t return anything (<code>return_void<\/code>), starts<br \/> immediately (<code>std::suspend_never initial_suspend<\/code>), and does not sleep upon<br \/> completion (<code>std::suspend_never final_suspend<\/code>).<\/p>\n<p>In some cases, such as when implementing unit tests, it is convenient to sleep<br \/> the coroutine upon completion:<\/p>\n<pre><code class=\"cpp\">struct TTestPromise;  struct TTestTask : std::coroutine_handle&lt;TTestPromise> {     using promise_type = TTestPromise; };  struct TTestPromise {     TTestTask get_return_object() {        return { TTestTask::from_promise(*this) };      }     std::suspend_never initial_suspend() { return {}; }     std::suspend_always \/* the difference here *\/ final_suspend() noexcept      { return {}; }     void return_void() {}     void unhandled_exception() {} }; <\/code><\/pre>\n<p>Please note that in this case, the <code>promise<\/code> differs only in the signature of<br \/> the <code>final_suspend<\/code> method.<\/p>\n<p><a class=\"anchor\" name=\"4\" id=\"4\"><\/a><\/p>\n<h3>Using final_suspend in Unit Tests<\/h3>\n<p>Let&#8217;s consider a unit test that launches client and server coroutines and then<br \/> checks the correctness of the message exchange:<\/p>\n<pre><code class=\"cpp\">void test_write_after_connect(void**) {     TLoop loop;     TSocket socket(TAddress{\"127.0.0.1\", 8898}, loop.Poller());     socket.Bind();     socket.Listen();     char send_buf[128] = \"Hello\";     char rcv_buf[128] = {0};      TTestTask h1 = [](TLoop* loop, char* buf, int size) -> TTestTask     {         TSocket client(TAddress{\"127.0.0.1\", 8898}, loop->Poller());         co_await client.Connect();         co_await client.WriteSome(buf, size);         co_return;     }(&amp;loop, send_buf, sizeof(send_buf));      TTestTask h2 = [](TSocket* socket, char* buf, int size) -> TTestTask     {         TSocket clientSocket = std::move(co_await socket->Accept());         co_await clientSocket.ReadSome(buf, size);         co_return;     }(&amp;socket, rcv_buf, sizeof(rcv_buf));      while (!(h1.done() &amp;&amp; h2.done())) {         loop.Step();     }     h1.destroy(); h2.destroy();      assert_true(memcmp(&amp;send_buf, &amp;rcv_buf, sizeof(send_buf))==0); } <\/code><\/pre>\n<p>In this case, the event handling loop runs until<\/p>\n<\/div>\n<\/div>\n<\/div>\n<\/div>\n","protected":false},"author":1,"featured_media":0,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[],"tags":[],"class_list":["post-357885","post","type-post","status-publish","format-standard","hentry"],"_links":{"self":[{"href":"https:\/\/savepearlharbor.com\/index.php?rest_route=\/wp\/v2\/posts\/357885","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/savepearlharbor.com\/index.php?rest_route=\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/savepearlharbor.com\/index.php?rest_route=\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/savepearlharbor.com\/index.php?rest_route=\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/savepearlharbor.com\/index.php?rest_route=%2Fwp%2Fv2%2Fcomments&post=357885"}],"version-history":[{"count":0,"href":"https:\/\/savepearlharbor.com\/index.php?rest_route=\/wp\/v2\/posts\/357885\/revisions"}],"wp:attachment":[{"href":"https:\/\/savepearlharbor.com\/index.php?rest_route=%2Fwp%2Fv2%2Fmedia&parent=357885"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/savepearlharbor.com\/index.php?rest_route=%2Fwp%2Fv2%2Fcategories&post=357885"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/savepearlharbor.com\/index.php?rest_route=%2Fwp%2Fv2%2Ftags&post=357885"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}