🚀 [協程與異步服務器實戰]:[C++20協程原理與Boost.Asio異步服務器開發]
📅 更新時間:2025年07月05日
🏷? 標簽:C++20 | 協程 | Boost.Asio | 異步編程 | 網絡服務器
文章目錄
- 前言
- 一、什么是協程?
- 二、線程與協程?
- 三、使用協程搭建異步服務器進行通信
- 1.服務端
- 1.流程圖
- 2.服務端代碼
- 1.主函數入口
- 2.lisenter 協程函數
- 3.echo協程函數
- 2.客戶端
- 四、測試
- 總結
前言
今天我們學習協程的基本概念,以及如何用協程來搭建一個簡單的異步服務器來進行與客戶端的收發數據
一、什么是協程?
協程(Coroutine)是一種比線程更輕量級的“并發編程”方式
它允許你在一個線程內,把任務分成多個可以掛起和恢復的小段,寫出“像同步一樣的異步代碼”。
協程的特點
可以在執行過程中主動暫停(掛起),等條件滿足后恢復執行
多個協程可以在同一個線程內切換,切換速度非常快
由程序員或框架調度,而不是操作系統
總結
協程就是可以隨時掛起和恢復的輕量級任務,讓你用很少的資源實現高效的并發和異步編程
二、線程與協程?
直觀比喻
線程像“多個人各自做事”
協程像“一個人做多件事,可以隨時暫停當前任務,去做別的,再回來繼續”
三、使用協程搭建異步服務器進行通信
1.服務端
1.流程圖
1.程序啟動(main)
初始化 io_context
設置信號處理(監聽 Ctrl+C 等信號,優雅退出)
啟動 lisenter
協程
2.lisenter 協程
創建 acceptor
,監聽 10086 端口
進入無限循環:
異步等待新連接(co_await acceptor.async_accept()
)
每有一個新連接,啟動一個 echo
協程處理該連接
3.echo 協程
進入無限循環:
異步讀取客戶端數據(co_await socket.async_read_some()
)
異步寫回數據(co_await async_write()
)
信號處理
收到終止信號時,調用 ioc.stop()
,優雅關閉服務器
2.服務端代碼
1.主函數入口
我們在主函數中利用 try
catch
來進行跑代碼,這樣可以防止后續的出錯,
我們先定義一個信號集signal_set
來實現服務器的優雅退出,當客戶端使用Ctrl+C
等操作的時候,我們可以通知 上下文 io_context
直接調用 .stop()
來暫停服務
int main()
{try{boost::asio::io_context ioc(1);boost::asio::signal_set signals(ioc, SIGINT, SIGTERM);signals.async_wait([&](auto,auto){ioc.stop();});co_spawn(ioc,lisenter(),detached);//啟動協程ioc.run();}catch(std::exception& e){std::cout << "main Exception is" << e.what() << std::endl;}
}
在寫 Lambda
表達式的時候如果要調用上下文io_context
必須用&
捕獲
核心原因是:io_context(
以及很多 Asio 相關對象)本身禁止拷貝,只能被引用捕獲,不能被值捕獲
co_spawn(ioc,lisenter(),detached);//啟動協程
co_spawn
co_spawn
表示啟動一個協程,參數分別為調度器,執行的函數,以及啟動方式, 比如我們啟動了一個協程,deatched
表示將協程對象分離出來,這種啟動方式可以啟動多個協程,他們都是獨立的,如何調度取決于調度器,在用戶的感知上更像是線程調度的模式,類似于并發運行,其實底層都是串行的
此時需要傳入三個參數
第一個參數
boost::asio::io_context
所有異步事件和協程都要綁定到某個 io_context,它負責調度和執行
第二個參數
是你自定義的協程函數,返回類型通常是 awaitable<void>
這里我們自定義的函數是lisenter()
第三個參數
協程的完成方式
總共有三種,分別是
detached
作用:協程分離運行,主程序不關心協程的返回值和異常。
用法:適合“只管啟動,不關心后續”的場景(如服務器監聽、后臺任務)
use_awaitable
作用:讓協程的結果可以被 co_await
等待,用于協程之間的嵌套和組合。
用法:適合你想在另一個協程里等待這個協程的結果
最后一種是自定義的回調函數
所以我們這句話
co_spawn(ioc,lisenter(),detached);//啟動協程
的完整意思就是
在 ioc 這個事件循環中,啟動一個 lisenter 協程,讓它自己運行,主程序不關心它的結果
2.lisenter 協程函數
我們定義一個協程函數,然后在協程函數中我們創建一個監聽器acceptor
進行綁定上下文和tcp協議和端口號
然后我們調用一個死循環,內部異步的進行監聽然后創建一個socket
,然后我們根據這個socket
再創建一個協程echo
單獨管理此客戶端的通信
awaitable<void> lisenter()
{auto executor = co_await this_coro::executor;//co_await異步獲取調度器tcp::acceptor acceptor(executor, { tcp::v4(),10086 });for (;;){tcp::socket socket = co_await acceptor.async_accept(use_awaitable);co_spawn(executor, echo(std::move(socket)), detached);//為每一個連接單獨啟動 一個協程進行收發數據}
}
如果要寫協程函數,必須是這種類型
awaitable<T>
我們在協程中進行了對當前協程獲取調度器的實現
auto executor = co_await this_coro::executor;//co_await異步獲取調度器
在 Boost.Asio
中,executor
是一個“執行環境”,負責調度和管理異步操作的執行
常見的 executor
有 io_context::executor_type
,它和 io_context
綁定
因為我們在主函數中使用了
co_spawn(ioc, lisenter(), detached)
啟動協程時,協程會自動和 ioc 綁定
但在協程體內,如果你要創建新的異步對象(如 acceptor
),需要明確告訴它用哪個 executor
,否則它不知道該和哪個事件循環關聯
所以相當于給這個監聽器綁定了一個上下文io_context
然后我們再來介紹一下
co_await
co_await
是 C++20 協程的通用關鍵字,它的作用是等待一個 “可等待對象” 完成,并獲取其結果
比如這里我們用來獲取當前協程的調度器和監聽器分配的socket
auto executor = co_await this_coro::executor;
tcp::socket socket = co_await acceptor.async_accept(use_awaitable);
3.echo協程函數
這個協程函數就是單獨為當前分配的客戶端進行讀寫通信的實現
awaitable<void>echo(tcp::socket socket)
{try{char data[1024];for (;;){std::size_t n=co_await socket.async_read_some(boost::asio::buffer(data), use_awaitable);co_await async_write(socket, boost::asio::buffer(data, n), use_awaitable);}}catch (std::exception& e){std::cout << "echo Exception is" << e.what() << std::endl;}
}
我們多次利用這個co_await實現了將看似同步的代碼,實現了異步等待的操作,比如這句
//獲取收到數據長度
std::size_t n=co_await
socket.async_read_some(boost::asio::buffer(data), use_awaitable);//異步寫
co_await async_write
(socket, boost::asio::buffer(data, n), use_awaitable);
2.客戶端
客戶端我們還是用以前的簡易的版本,發送 hello world
進行測試,不考慮其他的問題
#include <iostream>
#include<boost/asio.hpp>using namespace std;
using namespace boost::asio::ip;
const int MAX_LENGTH = 1024;int main()
{try{boost::asio::io_context ioc;tcp::endpoint remote_ep(boost::asio::ip::make_address("127.0.0.1"), 10086);tcp::socket sock(ioc);boost::system::error_code error = boost::asio::error::host_not_found;sock.connect(remote_ep, error);if (error){cout << "connect failed, code is" << error.value() <<" error msg is "<<error.what() << endl;return 0;}cout << "Enter Message:" << endl;char request[MAX_LENGTH];cin.getline(request, MAX_LENGTH);size_t request_len = strlen(request);boost::asio::write(sock, boost::asio::buffer(request, request_len));char reply[MAX_LENGTH];size_t reply_len = boost::asio::read(sock, boost::asio::buffer(reply, request_len));cout << "reply is" << string(reply,reply_len) << endl;getchar();}catch (std::exception& e){std::cout << "main exception is " << e.what() << std::endl;}return 0;
}
四、測試
客戶端成功與服務器進行通信
總結
學習了協程了相關概念,以及如何利用協程來搭建一個簡易的異步服務器的小demo