ASIO 避坑指南:高效、安全與穩健的異步網絡編程

ASIO 避坑指南:高效、安全與穩健的異步網絡編程

引言

ASIO是很強大的一個異步io庫,但服務器除了高效以外,穩定也是關鍵,這篇文章主要總結了ASIO使用遇到的典型問題和坑:

  • 如何榨干io_context的性能,讓CPU和網卡持續飽和工作?
  • 如何安全地關閉一個連接,避免資源泄漏或程序崩潰?(特別是當異步操作還在進行時)
  • 如何正確地實現一個異步寫操作,確保數據完整發送且內存安全?
  • 如何管理跨越多個異步操作的對象生命周期
  • 如何設計緩沖區(Buffer) 才能避免懸垂指針或數據競爭?
  • 如何在多線程環境下安全地操作共享資源?
  • 如何處理錯誤碼,哪些錯誤碼要特殊處理,例如operation_abortedeof

上面這些問題處理不好,輕則導致性能低下、資源泄漏,重則引發程序崩潰、數據錯誤,是基于ASIO開發服務器必須要了解清楚的點。


一、最大化利用 I/O (榨干 io_context 的性能)

很多人包括很多博客對asio的多線程操作僅僅照搬例子進行介紹,沒有真正的線上實戰,asio多線程有兩種方法:

  1. 單一 I/O 上下文 + 多工作線程

    • io_context 實例
    • 多個線程調用 io_context.run()
    • 事件分發機制:操作系統將就緒事件分配給不同工作線程執行
  2. 多 I/O 上下文 (io_context per Thread)

    • 每個線程獨占一個 io_context 實例
    • 每個線程調用自己 io_context.run()
    • 資源隔離:Socket/Timer 綁定到特定線程的 io_context

io_context 的核心是事件循環。一個線程調用 run() 通常足以高效處理數千連接,如果多個線程都執行 run() ,那么事件會分配到多個線程中執行,這樣你首先要考慮的是線程安全,每個回調如果調用了共享資源都需要枷鎖,這會降低運行效率。

單一 I/O 上下文 + 多工作線程

// 創建線程池執行同一個io_context
asio::io_context io;
asio::thread_pool pool(4); // 4個線程// 多個線程執行同一個io_context的run()
for(int i=0; i<4; ++i){asio::post(pool, [&]{ io.run(); });
}// 注意:所有Handler可能在不同線程執行!
socket.async_read_some(..., [](...){// 需要線程同步!可能被任意線程執行
});

優勢

  • 最佳負載均衡:內核自動分配事件到空閑線程
  • 簡化資源管理:所有操作共享單一I/O上下文

劣勢

  • 鎖競爭開銷:共享資源訪問需要同步,抵消多線程收益

ASIO針對這種情況提供了 strand 進行序列化訪問

例如:

// 創建strand綁定到io_context
asio::strand<asio::io_context::executor_type> my_strand = asio::make_strand(io.get_executor());// 通過strand分發處理程序
socket.async_read_some(asio::bind_executor(my_strand, [](...){// 保證同一strand上的handler不會并發執行connections.erase(id); // 無需鎖!}
));

每線程獨立 I/O 上下文 (io_context per Thread)

這是推薦做法,經過本人驗證,能極大提高并發處理能力

// 每個線程擁有獨立io_context
std::vector<std::unique_ptr<asio::io_context>> io_contexts;
std::vector<std::thread> threads;for(int i=0; i<4; ++i){io_contexts.emplace_back(std::make_unique<asio::io_context>());threads.emplace_back([ioc=io_contexts.back().get()]{ioc->run(); // 每個線程運行自己的io_context});
}// 連接綁定到特定io_context
auto& io = *io_contexts[connection_id % 4];
tcp::socket socket(io);

優勢

  • 處理程序始終在同一線程執行,避免線程切換開銷
  • 能更大程度發揮單個io的性能

保證異步操作鏈的持續

一個異步操作完成時,在其完成處理函數 (Completion Handler) 中發起下一個異步操作(如 async_read 后發起 async_write,或繼續 async_read),這樣可以保持 I/O 通道持續忙碌,避免輪詢

要注意的是,一定要避免在 回調 中做耗時同步操作阻塞事件循環。

高效 Buffer 管理

asio::buffer 是視圖: 它不擁有數據,只是指向現有內存塊的引用,處理不當會導致野指針、數據損壞或程序崩潰。底層數據必須在異步操作期間保持有效!

絕對要避免使用棧分配的內容做作為 Buffer,例如下面這個就是典型的錯誤:

//錯誤示范
void do_async_write(tcp::socket& socket) {char buffer[1024]; // 錯誤:棧分配緩沖區generate_data(buffer, 1024); // 填充數據// 異步寫操作 - 緩沖區可能在函數返回后失效!socket.async_write_some(asio::buffer(buffer, 1024),[](const asio::error_code& ec, size_t bytes) {// 此時原buffer棧幀已銷毀 - 野指針訪問!});
} // 函數退出,棧緩沖區被銷毀!

除非你用的是協程模式,否則不要用棧分配內存做 Buffer,因為異步操作結束后,棧內存會被回收,Buffer 就會變成無效的,過一段時間在執行回調你的buffer里面就是野指針,因此要嚴格保證 async_read/async_write 使用的 buffer 底層內存在其整個操作期間(從調用開始到 Handler 執行結束)有效且不被修改

你應該使用智能指針來分配緩沖,并讓這個智能指針跟隨回調函數,直至回調函數結束,典型的就是讓lambda把這個智能指針捕獲,讓它跟著回調函數的生命周期。

void send_large_data(tcp::socket& socket) {// 使用shared_ptr管理堆緩沖區auto buf = std::make_shared<std::vector<char>>(generate_large_data());asio::async_write(socket, asio::buffer(*buf),// 捕獲智能指針延長生命周期[buf](const asio::error_code& ec, size_t) {// 緩沖區在lambda銷毀前保持有效});
}

或者作為session的成員變量

class Connection : public std::enable_shared_from_this<Connection> {std::array<char, 8192> buffer_; // 成員緩沖區void start_read() {auto self(shared_from_this());socket_.async_read_some(asio::buffer(buffer_),[self](const asio::error_code& ec, size_t length) {if (!ec) self->process_data(length);});}
};

如果是linux系統,還可以用零拷貝緩沖區注冊方法,讓io和回調都操作這個緩沖區,從而避免了數據拷貝。

// 注冊持久內存到io_context
auto buf = std::make_shared<std::array<char, 4096>>();
asio::io_context& ioc = socket.get_executor().context();// 顯式注冊緩沖區(Linux專屬優化)
const bool registered = asio::register_buffer(ioc, asio::buffer(*buf), asio::buffer_registration::permanent);socket.async_read_some(asio::buffer(*buf),[buf](const asio::error_code& ec, size_t bytes) {// 緩沖區保持注冊狀態});

這種尤其適合高頻小包數據的處理

安全關閉 Socket 和連接

關閉是異步編程中最容易出資源泄漏或崩潰的地方。關閉做的不好,會出現如下問題:

  • 資源泄漏(文件描述符、內存)
  • 大量CLOSE_WAIT狀態連接
  • 程序崩潰(訪問已銷毀對象)
  • 數據丟失(未發送完的數據)

Socket的關閉有shutdown()close()兩個行數

socket.shutdown

shutdown可以理解為是關閉通知,有三種模式(shutdown_receive, shutdown_send, shutdown_both),通知對端“我不會再發數據了”(shutdown_send)或“我不想再收數據了”(shutdown_receive)。

shutdown執行后,后續的 async_read 會立即完成并返回 asio::error::shut_down (如果接收端關閉),后續的 async_write 會立即完成并返回 asio::error::shut_down (如果發送端關閉)。

需要注意的是,shutdown()后,Socket 描述符依然有效。

socket.close

socket.close會釋放系統資源(Socket 描述符),它會隱式地執行 shutdown(both)。任何掛起(Pending)的異步操作(async_read, async_write, async_connect 等)會立即取消,它們的回調函數會被調用,并傳入 asio::error::operation_aborted 錯誤碼。

因此,在讀寫回調中,遇到asio::error::operation_aborted 錯誤碼要特殊處理,避免重復關閉

回調函數設計時,應檢查錯誤碼,如果是 operation_aborted,通常意味著 Socket 正在被關閉/銷毀,回調函數應該:

  • 忽略這個操作的結果。
  • 清理相關的資源(如釋放為這次操作分配的 Buffer)。
  • 避免再訪問Socket

當調用 socket.close() 取消操作時,包含 socket 的對象(如 connection)可能正在被銷毀

安全關閉方法

關閉分服務器主動關閉,以及對方客戶端主動關閉,兩種不同方式的關閉處理方式不太一樣

  • 服務器主動關閉
  1. 標記關閉開始,執行shutdown(socket, asio::ip::tcp::socket::shutdown_receive); // 告訴對方我不再接收數據了
  2. 檢查是否有待發送數據,無數據 → 立即關閉,有數據 → 等待當前寫操作完成
// 在管理類中觸發關閉
void ConnectionManager::stop_all() {for (auto& conn : connections_) {conn->safe_shutdown();}
}// Connection::safe_shutdown實現:
void safe_shutdown() {if (shutdown_initiated_.exchange(true)) return;// 1. 停止接收新數據socket_.shutdown(shutdown_receive);// 2. 檢查寫狀態if (!writing_) {final_close();  // 無待發數據直接關閉}// 否則等待進行中的寫操作完成
}
  • 客戶端主動關閉
  1. async_read Handler 中檢測到 error_code == asio::error::eof (對方正常關閉發送端)
  2. (可選)如果還有數據要發送,可以嘗試發送(但對方可能已關閉接收端,會出錯)。
  3. 調用 socket.close()

下面是一個客戶端的安全關閉示例:

void Connection::handle_read_error(asio::error_code ec) {if (ec == asio::error::eof) {// 客戶端發送了FIN包safe_shutdown();}else if (ec == asio::error::operation_aborted) {// 正常關閉過程中的取消// 不進行任何操作,連接即將銷毀return;}
}

因此,一個安全的關閉不僅僅是close,還要針對不同的錯誤碼來執行不同的關閉策略,在接收和發送的錯誤碼處理不一樣,建議一個回話應該對錯誤碼處理單獨提取一個函數,如下:

class Connection : public std::enable_shared_from_this<Connection> {
private:asio::ip::tcp::socket socket_;std::queue<std::vector<char>> write_queue_;bool writing_;std::atomic<bool> shutdown_initiated_;std::array<char, 1024> read_buffer_;
public:Connection(asio::ip::tcp::socket socket): socket_(std::move(socket)), writing_(false),shutdown_initiated_(false) {}void start() {read_header();  // 開始讀循環}void safe_shutdown() {if (shutdown_initiated_.exchange(true)) return;// 1. 停止接受新數據asio::error_code ec;socket_.shutdown(asio::ip::tcp::socket::shutdown_receive, ec);// 忽略錯誤:可能已關閉// 2. 檢查寫隊列if (!writing_) {// 無待發送數據,直接關閉final_close();} else {// 等待進行中的寫操作完成// final_close將在寫回調中調用}}private:void read_header() {auto self(shared_from_this());socket_.async_read_some(asio::buffer(read_buffer_),[this, self](asio::error_code ec, size_t length) {if (ec) {handle_read_error(ec);return;}process_data(length);read_header();  // 繼續讀});}void handle_read_error(asio::error_code ec) {if (ec == asio::error::eof) {// 客戶端正常關閉safe_shutdown();} else if (ec == asio::error::operation_aborted) {// 關閉過程中的正常取消} else {// 其他錯誤final_close();}}void async_write_data(std::vector<char> data) {// 將數據加入隊列bool write_in_progress = !write_queue_.empty();write_queue_.push(std::move(data));if (!write_in_progress && !writing_) {start_write_chain();}}void start_write_chain() {writing_ = true;auto self(shared_from_this());auto& buf = write_queue_.front();asio::async_write(socket_, asio::buffer(buf),[this, self](asio::error_code ec, size_t /*bytes*/) {writing_ = false;if (ec) {handle_write_error(ec);return;}write_queue_.pop();if (!write_queue_.empty()) {start_write_chain();} else if (shutdown_initiated_) {// 所有數據已發送,安全關閉final_close();}});}void handle_write_error(asio::error_code ec) {if (ec == asio::error::operation_aborted) {// 正常取消,忽略} else if (ec == asio::error::broken_pipe || ec == asio::error::connection_reset) {// 連接已斷開final_close();} else {// 其他錯誤處理safe_shutdown();}}void final_close() {asio::error_code ignore_ec;// 取消所有異步操作socket_.cancel(ignore_ec);// 關閉socketsocket_.shutdown(asio::ip::tcp::socket::shutdown_both, ignore_ec);socket_.close(ignore_ec);// 清理資源decltype(write_queue_) empty;std::swap(write_queue_, empty);}
};

上面的例子不僅展示了安全關閉,還包含了安全發送,關閉和發生接收關聯緊密,因此很難用一句函數就能涵蓋,上面的例子的發送用來一個隊列,這引出下一節,如何用asio進行高并發的安全的異步寫操作

安全的異步寫操作

除了上面提到的buffer有效性外,異步寫操作有其特定要點:

  • 同一 Socket 上的多個并發 async_write 操作是未定義行為! TCP 是流協議,數據順序必須保證。
  • 必須使用隊列(見上面的例子)。同一時間只允許一個 async_write 操作在進行,等回調完了之后,再寫入下一個

寫回調應該作如下工作:

  • 檢查 error_code (包括 operation_aborted,具體見上節安全關閉)。
  • 處理 bytes_transferred (通常成功時等于請求量)。
  • 釋放或標記該次寫操作使用的 Buffer 可重用/釋放。
  • **檢查寫隊列:如果隊列非空,取出下一批數據發起新的async_write,如果隊列為空,設置 writing_ = false

如上面的例子所示,安全大并發的異步寫操作,應該如下:

class Connection : public std::enable_shared_from_this<Connection> {
private:asio::ip::tcp::socket socket_;std::queue<std::vector<char>> write_queue_;bool writing_;std::atomic<bool> shutdown_initiated_;std::array<char, 1024> read_buffer_;
public://這里省略其他函數,在安全關閉里已經展示完整代碼void async_write_data(std::vector<char> data) {// 將數據加入隊列bool write_in_progress = !write_queue_.empty();write_queue_.push(std::move(data));if (!write_in_progress && !writing_) {start_write_chain();}}void start_write_chain() {writing_ = true;auto self(shared_from_this());auto& buf = write_queue_.front();asio::async_write(socket_, asio::buffer(buf),[this, self](asio::error_code ec, size_t /*bytes*/) {writing_ = false;if (ec) {handle_write_error(ec);return;}write_queue_.pop();if (!write_queue_.empty()) {start_write_chain();} else if (shutdown_initiated_) {// 所有數據已發送,安全關閉final_close();}});}void handle_write_error(asio::error_code ec) {if (ec == asio::error::operation_aborted) {// 正常取消,忽略} else if (ec == asio::error::broken_pipe || ec == asio::error::connection_reset) {// 連接已斷開final_close();} else {// 其他錯誤處理safe_shutdown();}}
};
  • 要有個寫隊列,示例中的std::queue<std::shared_ptr<std::string>> write_queue_;
  • 寫狀態標記writing_,主要作用是在關閉時,檢查是否還沒寫完,沒寫完就等寫完再關閉socket
  • 關閉標記shutdown_initiated_,這個主要作用是關閉標記,如果寫完發現已經關閉,直接調用close,通過shutdown_initiated_writing_可以實現安全的關閉,同時保證寫數據不會丟失

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/news/912000.shtml
繁體地址,請注明出處:http://hk.pswp.cn/news/912000.shtml
英文地址,請注明出處:http://en.pswp.cn/news/912000.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

鯨孿生中三維模型的常見問題~

鯨孿生是山海鯨中專門負責3D 場景搭建和渲染的組件&#xff0c;可以雙擊進入編輯&#xff0c;進入編輯之后組件欄也會跟著變化&#xff0c;可以插入更多的 3D 內部的組件。 搭建三維場景經常會使用到模型&#xff0c;包括人物模型、建筑物模型、汽車模型等&#xff0c;這些都可…

PyTorch中實現開立方

技術背景 在PyTorch中&#xff0c;沒有直接實現cbrt這一算子。這個算子是用于計算一個數的開立方&#xff0c;例如&#xff0c;最簡單的-8開立方就是-2。但這里有個問題是&#xff0c;在PyTorch中&#xff0c;因為沒有cbrt算子&#xff0c;如果直接用冪次計算去操作數字&#x…

關于如何在 Git 中切換到之前創建的分支的方法

文章目錄 關于如何在 Git 中切換到之前創建的分支的方法一、確保你在項目目錄中二、查看所有分支&#xff08;可選&#xff09;三、切換到目標分支四、如果分支僅在遠程存在五、驗證是否切換成功六、常見問題處理七、總結命令流程 PS:下次進入分支時&#xff0c;只需完成步驟1 …

基于深度學習的智能圖像語義分割系統:技術與實踐

前言 圖像語義分割是計算機視覺領域中的一個重要任務&#xff0c;其目標是將圖像中的每個像素分配到預定義的語義類別中。這一技術在自動駕駛、醫學影像分析、機器人視覺等多個領域有著廣泛的應用。近年來&#xff0c;深度學習技術&#xff0c;尤其是卷積神經網絡&#xff08;C…

歷史軌跡組件性能優化方案

針對歷史軌跡組件的性能優化&#xff0c;可從數據處理、渲染策略、內存管理和交互優化四個方面入手。以下是具體的優化方向和實現方案&#xff1a; 一、數據處理優化 1. 軌跡數據抽稀算法 原理&#xff1a;在不影響軌跡整體形狀的前提下&#xff0c;減少軌跡點數量實現方案&…

【論文閱讀36】- Graph Attention Network(2025)

這篇論文主要介紹了一種基于改進型圖注意力網絡&#xff08;Graph Attention Network, GAT&#xff09;的滑坡變形異質性監測方法。該方法通過融合多尺度時間嵌入和自適應圖學習&#xff0c;能夠同時捕捉監測點之間復雜的時空依賴關系&#xff0c;有效反映滑坡的局部與整體變形…

CSS基礎3

動畫-animation 動畫-animation與 transition過渡動畫的區別 transition過渡動畫&#xff1a;實現兩個狀態間的變化過程動畫animation&#xff1a;實現多個狀態間的變化過程&#xff0c;動畫過程可控&#xff08;重復播放、最終畫面、是否暫停&#xff09; 走馬燈-使用transiti…

Java 程序設計試題?

?考試時間&#xff1a;120 分鐘? ?總分&#xff1a;100 分? 一、選擇題&#xff08;每題 2 分&#xff0c;共 30 分&#xff09; 1.以下哪個不是 Java 的關鍵字&#xff1f; A. final B. sizeof C. static D. void 2.以下代碼輸出結果是&#xff1f; System.out.printl…

Elasticsearch(ES)與 OpenSearch(OS)

Elasticsearch&#xff08;ES&#xff09;與 OpenSearch&#xff08;OS&#xff09;本質上是同源分叉、獨立演進的技術&#xff0c;兩者關系可概括為“起源相同、目標分化”。以下是關鍵要點解析&#xff1a; &#x1f50d; 一、核心關系&#xff1a;分叉與獨立演進 起源相同 O…

Python爬蟲實戰:研究Ghost.py相關技術

1 引言 1.1 研究背景與意義 隨著互聯網技術的不斷發展,現代網頁越來越多地采用 JavaScript 動態生成內容,傳統的靜態爬蟲技術已難以滿足需求。例如,許多新聞網站的評論區、電商平臺的商品列表以及社交網站的動態內容均通過 AJAX 異步加載,普通爬蟲無法獲取這些內容。Ghos…

PostgreSQL(知識片):查詢/計算Selectivity(可選性)

一、視圖pg_ststs查詢可選性 1、當可選性較小時&#xff0c;可以用視圖pg_ststs來查詢 表的每一列的MVC&#xff08;most Common Value&#xff09;作為一對most_common_vals和most_common_freqs的列存儲在pg_ststs視圖中。 &#xff08;1&#xff09;most_common_vals&#x…

Android Studio 打 APK 包報錯 Invalid keystore format 的解決方法

提示&#xff1a;“奔跑吧鄧鄧子” 的必備核心技能專欄聚焦計算機技術與職場場景&#xff0c;拆解程序員、產品經理等技術從業者的核心能力圖譜。內容涵蓋編程思維、算法實戰、項目管理、技術架構等硬核技能&#xff0c;結合案例解析代碼優化、跨團隊協作等落地方法論。定期更新…

通義靈碼2.5智能體模式實戰———集成高德MCP 10分鐘生成周邊服務地圖應用

1 引言 在當今快節奏的開發環境中&#xff0c;智能編程助手正成為開發者生產力的倍增器。通義靈碼2.5的智能體模式通過任務分解、多輪對話和上下文感知&#xff0c;將傳統代碼補全提升為完整的解決方案生成能力。本文將以實戰案例展示如何利用通義靈碼2.5集成高德地圖MCP服務&…

【Linux】使用ip link命令設置bond

目錄 1、介紹2、設置步驟【1】創建bonding接口【2】設置bonding模式【3】添加物理網口到bonding接口【4】激活bonding接口 3、解除步驟【1】關閉bond接口【2】接觸從屬接口【3】刪除bond接口 1、介紹 設置bond的方法有很多種&#xff0c;其中通過命令行ip link設置就是其中一種…

Camunda相關表結構和字段備注SQL腳本

Camunda相關表結構和字段備注SQL腳本 引camunda engine表和字段備注 引 Camunda engine服務啟動時會自動創建相關的表&#xff0c;沿用了activity的設計&#xff0c;我這里使用的是7.17.0版&#xff0c;自動生成了49張表&#xff0c;但所有的表和字段都沒有備注信息&#xff0c…

Qt、C++自定義按鈕、組件、事件編程開發練習,萬字實戰解析!!

x項目地址&#xff1a;https://gitee.com/fan-wenshan/qt_learn_button-andevent_zhengzhuo 項目界面截圖&#xff1a; ### 項目介紹&#xff1a;comstomSingal (Qt應用程序) 項目基本信息 - 項目類型 &#xff1a;Qt Widgets應用程序 - 開發環境 &#xff1a;Qt 5.12.12 Min…

商務年度總結匯報PPT模版分享

商務匯報&#xff0c;工作總結&#xff0c;畢業答辯&#xff0c;簡歷競聘PPT模版&#xff0c;創意年終匯報PPT模版&#xff0c;IDEAS商務匯報PPT模版&#xff0c;年度總結PPT模版&#xff0c;創意低多邊形PPT模版&#xff0c;商務型PPT模版&#xff0c;小清新創意花朵PPT模版&a…

電機設計仿真軟件學習DAY3——Maxwell界面功能+3D幾何模型繪制

"手把手教你玩轉電機&#xff01;每日更新教程&#xff0c;評論區答疑解惑&#xff0c;小白也能變大神&#xff01;" 目錄 maxwell基礎操作 一.Maxwell基礎操作&#xff1a;新建項目 二.maxwell3D界面 三.maxwell3D繪圖 3.1繪制圓柱體的方法 3.2繪制正方體的方法…

Apache 支持 HTTPS

證書文件 提取私鑰 openssl pkcs12 -in cert.pfx -nocerts -out private.key -nodes 打開命令行&#xff08;CMD 或 PowerShell&#xff09;&#xff0c;進入證書所在目錄&#xff0c;輸入上面命令&#xff0c;它會提示你輸入密碼&#xff0c;可以從 password.txt 中復制 提取證…

自然語言處理中的Transformer模型:超越RNN和LSTM

在人工智能的眾多領域中,**自然語言處理(Natural Language Processing, NLP)**無疑是最具挑戰性也最具前景的方向之一。從機器翻譯、文本摘要到情感分析和智能問答,NLP 旨在讓機器理解、解釋和生成人類語言。長期以來,循環神經網絡(Recurrent Neural Network, RNN)及其變…