原因
大多數程序以某種方式與外界交互,無論是通過文件、網絡、串行電纜還是控制台。 有時,就像網絡一樣,單個 I/O 操作可能需要很長時間才能完成。 這對應用程序開發提出了特殊的挑戰。
Boost.Asio 提供了管理這些長時間運行的操作的工具,而無需程序使用基於線程和顯式加鎖的並發模型。
Boost.Asio 庫適用於使用 C++ 進行系統編程的程序員,這些程序員通常需要訪問操作系統功能,例如網絡。 特別是,Boost.Asio 解決了以下目標:
- 可移植性。該庫支持一系列常用的操作系統,並為這些操作系統提供一致性的行為。
- 伸縮性。該庫應用來開發能擴展到數千個並發連接的網絡應用程序。每個操作系統的庫實現應該使用最能實現這種可伸縮性的機制。
- 高效。該庫應支持分散-聚集 I/O 等技術,並允許程序最大限度地減少數據復制。
- 模型概念來自已有的API,如BSD套接字。BSD套接字API被廣泛實現和理解,並在很多文獻都有涉及。其他編程語言通常使用相似的網絡接口API。在合理的情況下,Boost.Asio應該利用現有的做法。
- 易用。該庫應該采用提供工具包而不是框架的方法為新用戶提供較低的入門門檻。也就是說,它應該盡量減少預先在時間上的投資,只學習一些基本的規則和指導方針。在此之后,庫用戶應該只需要了解正在使用的特定函數。
- 進一步抽象的基礎。該庫應該允許能夠開發提供更高級別抽象的庫。例如,常用協議HTTP的實現。
盡管 Boost.Asio 一開始主要關注網絡,但它的異步 I/O 概念已經擴展到包括其他操作系統資源,例如串行端口、文件描述符等。
核心概念和功能
Asio的基本架構
Asio可用於對IO對象(如套接字)執行同步和異步操作。在使用 Boost.Asio 之前,了解一下 Boost.Asio 的各個部分、您的程序以及它們如何協同工作的概念圖可能會很有用。
作為入門級的示例,讓我們考慮在套接字上執行連接操作時會發生什么。 我們將從檢查同步操作開始。
你的程序中應該至少有一個IO執行上下文(I/O Execution Context),例如一個boost::asio::io_context對象,boost::asio::thread_pool對象或者boost::asio::system_context對象。I/O可執行上下文代表了你的程序與操作系統的I/O服務的鏈接。
boost::asio::io_context io_context;
為了執行I/O操作,你的程序需要一個I/O對象,例如TCP套接字:
boost::asio::ip::tcp::socket socket(io_context);
當執行一個同步的連接操作時,會發生下面的事件時序:
- 你的程度通過調用I/O對象來啟動連接操作。
socket.connect(server_endpoint);
- IO對象將請求轉發到IO execution context。
- IO execution context調用操作系統來執行連接操作。
- 操作系統返回執行結果給IO execution context。
- IO execution context將任何運行的錯誤結果轉為boost::system::error_code類型。error_code可以與特定值進行比較,或作為布爾值進行測試(為false表示沒有發生錯誤)。結果轉發會IO對象。
- 如果操作失敗,IO對象拋出一個boost::system::system_error類型的異常。如果初始化操作的代碼被寫為:
boost::system::error_code ec;
socket.connect(server_endpoint,ec);
那么error_code類型變量ec將會被設置為運行結果,不會拋出異常。
當使用異步操作的時候,發生的事件時序有所不同。
- 你的程序通過調用IO對象啟動連接操作:
socket.async_connect(server_endpoint,completion_handler);
completion_handler是一個函數並且函數簽名如下:
void completion_handler(const boost::system::error_code ec);
所需的確切簽名取決於正在執行的異步操作。參考文檔指出了每個操作的適當形式。
- IO對象轉發請求到IO execution context。
- IO execution context向操作系統發送信號,表示它應該啟動一個異步連接。不會阻塞在連接操作處。 (在同步情況下,此等待將完全包含在連接操作的持續時間內。)
- 操作系統通過將結果放入隊列,准備被IO execution context取走來指示連接操作已經完成。
- 當使用io_context作為IO execution context,你的程序必須調用io_context::run()(或一個類似io_context的成員函數)來獲取結果。調用io_context::run(),只要還有未完成的異步操作就會阻塞,所以只要你啟動了你的第一個異步操作你就應該立即調用。
- 在調用io_context::run()時,IO execution context取出運行結果,將其轉為error_code,然后傳遞給completion handler。
Proactor設計模式:無線程的並發
Asio為同步操作和異步操作提供的並行支持。異步操作是基於Proactor設計模式。與只有同步和Reactor方式相比,這種模式的優缺點如下所述:
Proactor與Asio
我們先了解一下Proactor在Asio中是如何實現的,但不設計具體的細節。
- Asynchronous Operation(異步操作):定義一個異步執行的操作,例如異步讀或寫一個套接字。
- Asynchronous Operation Processor(異步操作處理器):執行異步操作,並且當操作完成時在完成事件隊列上對事件進行排隊。
- Completion Event Queue(完成事件隊列):緩存完成事件直到他們被異步事件分發器取走。
- Completion Handler(完成處理器):處理異步操作的結果。這些是函數對象,通常是使用boost::bind創建的。
- Asynchronous Event Demultiplexer(異步事件分配器):阻塞等待完成事件隊列上有事件出現,並將完成的時間返回給調用者。
- Proactor:調用異步事件分配器來取出事件,然后調度與事件關聯的完成處理器(即調用函數對象)。抽象為io_context類。
- Initiator(啟動器):啟動異步操作的應用程序代碼。Initiator通過高級接口(如basic_stream_socket)與異步操作處理器交互,該接口反過來委托給像reactive_socket_service的服務。
使用Reactor實現
在許多平台,Asio使用Reactor實現Proactor設計模式,例如select,epoll或者kqueue。這種實現方法對應的Proactor設計模式如下:
- Asynchronous Operation Processor:Reactor實現使用select,epoll或kqueue。當Reactor指示執行操作的資源准備好時,處理器執行異步操作並將相關的完成處理器入隊到完成事件隊列中。
- Completion Event Queue:一個完成處理器的鏈表。
- Asynchronous Event Demultiplexer:這個是通過等待一個事件或條件變量直到完成事件隊列中有一個完成處理器可用實現的。
使用Windows Overlapped I/O實現
在Windows NT,2000和XP。Asio利用Overlapped I/O的優點提供了高效的Proactor設計模式的實現。這種實現方式對應的Proactor設計模式如下:
- Asynchronous Operation Processor:這是通過操作系統實現的。操作是通過調用諸如 AcceptEx 之類的重疊函數來啟動的。
- Completion Event Queue:這是由操作系統實現的,並與I/O完成端口相關聯。每個io_context實例都有一個I/O完成端口。
- Asynchronous Event Demultiplexer:由 Asio 調用以使事件及其關聯的完成處理程序出列。
優點
- 可移植性。許多操作系統提供本機異步 I/O API(例如Windows上的重疊 I/O )作為開發高性能網絡應用程序的首選選項。該庫可以根據本機異步 I/O 來實現。但是,如果本機支持不可用,也可以使用代表 Reactor 模式的同步事件多路分解器(例如POSIX
select()
) 來實現該庫。 - 將線程和並發解耦。長時間操作是由代表應用程序的實現來異步執行的,因此,應用程序不需要產生許多線程來增加並發性。
- 高效和可伸縮性。由於增加了 CPU 之間的上下文切換、同步和數據移動,諸如thread-per-connect(僅同步方法需要)之類的實現策略可能會降低系統性能。 通過異步操作,可以通過最小化操作系統線程的數量並且只激活有事件要處理的邏輯控制線程來避免上下文切換的成本。
- 簡化應用程序同步。可以編寫異步操作完成處理程序,就好像它們存在於單線程環境中一樣,因此可以在開發應用程序邏輯時幾乎不關心同步問題。
- 函數組合。函數組合是指實現以提供更高級操作的函數,例如以特定格式發送消息。 每個函數都是對較低級別的讀取或寫入操作的多次調用來實現的。例如,考慮這樣一種協議,其中每個消息由固定長度的報頭和可變長度的正文組成,其中正文的長度在報頭中指定。假設的讀取消息操作可以使用兩個較低級別的讀取來實現,第一個讀取用於接收消息頭,一旦長度已知,第二個讀取用於接收消息體。要在異步模型中組合函數,可以將異步操作鏈接在一起。也就是說,一個操作的完成處理程序可以啟動下一個操作。啟動鏈中的第一個調用可以被封裝,這樣調用者就不需要知道高級操作是作為異步操作鏈實現的。以這種方式組合新操作的能力簡化了在網絡庫之上的更高抽象級別的開發,例如支持特定協議的函數。
缺點
-
程序復雜度。由於操作啟動和完成之間的時間和空間分離,使用異步機制開發應用程序更加困難。 由於反向控制流,應用程序也可能更難調試。
-
內存利用率。在讀或寫操作期間必須提交緩沖區空間,這可能會無限期地持續下去,並且每個並發操作都需要一個單獨的緩沖區。 另一方面,Ractor模式在套接字准備好讀取或寫入之前不需要緩沖區空間。
線程和Asio
線程安全
一般來說,並發使用不同的對象是安全的,但是並發使用單個對象是不安全的。但是,諸如io_context之類的類型提供了強力的保證,即並發使用單個對象是安全的。
線程池
多個線程可以調用 io_context::run() 來設置一個線程池,可以從中調用完成處理器。這種方法也可以與post()一起使用,作為跨線程池執行任意計算任務的方法。
請注意,所有加入io_context的線程都被認為是等價的,io_context可以以任意的方式在它們之間分發工作。
內部線程
這個庫對於特定平台的實現可以使用一個或多個內部線程來模擬異步性。這些線程對於庫的使用者必須盡可能不可見。特別的,這些線程:
- 一定不能直接調用用戶代碼
- 必須屏蔽所有信號
此方法由以下保證補充:
- 異步完成處理器只會被當前正在調用 io_context::run() 的線程調用。
因此,庫用戶有責任創建和管理通知將發送到的所有線程。
采用這種方法的原因包括:
- 通過僅從單個線程調用io_context::run(),用戶代碼可以避免與同步相關的開發復雜度。例如,庫用戶可以實現單線程的可擴展服務器(從用戶角度看)。
- 庫用戶可能需要在線程啟動后不久和任何其他應用程序代碼執行之前在線程中執行初始化。例如,Microsoft 的 COM 用戶必須先調用 CoInitializeEx,然后才能從該線程調用任何其他 COM 操作。
- 庫接口與線程創建和管理的接口解耦,並允許在線程不可用的平台上實現。
Strands:使用線程而不使用顯式加鎖
Strands被定義為事件處理程序的嚴格調用。使用Strands允許在多線程程序中執行代碼而無需顯式加鎖。
Strands可以是顯式的,也可以是隱式的。如下面的例子:
- 只在一個線程調用io_context::run()意味着所有的事件處理器在一個隱式strand中,因為io_context保證處理器只能從run()中調用。
- 如果存在與連接相關聯的單個異步操作鏈(例如,在像 HTTP 這樣的半雙工協議實現中),則不可能同時執行處理器。 這是一個隱含的鏈。
- 顯式的strand是
strand<>
或者io_context::strand
的實例。所有的事件處理器函數對象需要使用boost::asio::bind_executor
綁定到strand上,或者使用strand對象進行發布/調度。
在異步操作組合的情況下,像async_read()
或者async_read_until()
,如果一個完成處理器經過了一個strand,那么所有的中間處理器也應該經過同樣的strand。這是確保能夠線程安全地訪問調用者和組合操作之間共享對象所必要的(在async_read()
的情況下,它是套接字,調用者可以close()
取消操作)。
為了實現這一點,所有的異步操作通過使用get_associated_executor
函數獲取處理器相關的執行器。例如:
boost::asio::associated_executor_t<Handler> a = boost::asio::get_associated_executor(h);
相關的執行器必須滿足Executor要求。異步操作使用它來提交中間和最終處理器以供執行。
可以通過指定嵌套類型executor_type
和成員函數get_executor
為特定處理器類型自定義執行器:
class my_handler {
public:
//Executor 類型要求的自定義實現。
typedef my_executor executor_type;
//返回一個自定義執行器的實現
executor_type get_executor() const noexcept {
return my_executor();
}
void operator()() {...}
};
在更復雜的情況下,associated_executor
模板可能會直接部分特化:
//處理器
struct my_handler {
void operator()() {...}
};
namespace boost {namespace asio {
//特化associator_executor模板
template<class Executor>
struct associated_executor<my_handler,Executor> {
//Executor 類型要求的自定義實現。
typedef my_executor type;
//返回一個自定義執行器的實現
static type get(const my_handler&,const Executor&=Executor()) noexcept {
return my_executor();
}
};
}}
boost::asio::bind_executor()
函數用來將特定的executor對象(像strand)綁定到一個完成處理器上。這個綁定會自動關聯一個執行器。例如,為了將strand綁定到一個完成處理器上,我們可以簡單地寫為:
my_socket.async_read_some(my_buffer,
boost::asio::bind_executor(my_strand,[](error_code ec,size_t length) {
//....
}));
Buffers
從根本上說,I/O涉及在內存的連續區域(稱為緩沖區)之間傳輸數據。
這些緩沖區可以簡單地表示為由一個指針和一個字節大小組成的元組。但是,為了開發高效的網絡應用程序,Asio包括對分散-聚集操作的支持。這些操作涉及一個或多個緩沖區。
- 分散讀將數據讀取到多個緩沖區
- 聚集寫,傳輸多個緩沖區
因為,我們需要一個表示緩沖區集合的抽象。Asio使用的方法就是定義一個(實際上是兩個)來表示單個緩沖區。這些可以存儲在一個容器中,而該容器可以傳遞給分散-聚集操作。
除了將緩沖區指定為指針和字節大小之外,Boost.Asio 還區分了可修改內存(稱為可變)和不可修改內存(后者是從 const 限定變量的存儲中創建的)。 因此,這兩種類型可以定義如下:
typedef std::pair<void*,std::size_t> mutable_buffer;
typedef std::pair<const void*,std::size_t> const_buffer;
mutable_buffer可以轉為const_buffer,但是不能反過來轉換。
但是,Asio並沒有使用上面的定義,而是定義了兩個類mutable_buffer
和const_buffer
。其目的是提供連續內存的不透明表示,其中:
- 類型的轉換行為與std::pair定義方式的表現一樣。也就是說
mutable_bufer
可以轉為const_buffer
,但是不能反過來。 - 有防止緩沖區溢出的保護。給定一個緩沖區實例,用戶只能創建表示相同內存范圍或其子范圍的另一個緩沖區。為了提供進一步的安全性,該庫還包括用於從數組中自動確定緩沖區大小的機制,POD元素的
boost::array
或std::vector
,或來自std::stirng
。 - 使用
data()
顯式訪問底層的內存。通常來說,應用程序不需要這樣做,但是庫的實現需要傳遞原始內存給底層操作系統函數。
最后,多個buffer可以通過將buffer對象放入容器中傳給分散-聚集操作(像read()
或write()
)。定義了MutableBufferSequence
和ConstBufferSequence
概念,以便可以使用像std::vector,std::list,std::array,boost::array
的容器。
與iostreams集成的streambuf
類boost::asio::basic_streambuf
派生自std::basic_streambuf
以將輸入序列和輸出序列與某種字符數組類型的一個或多個對象相關聯,這些對象的元素存儲任意值。這些字符數組對象在 streambuf 對象內部,但提供了對數組元素的直接訪問,以允許它們與 I/O 操作一起使用,例如套接字的發送或接收操作:
- streambuf的輸入序列可以通過
data()
成員函數訪問。該函數的返回類型滿足ConstBufferSequence
的要求。 - streambuf的輸出序列可以通過
prepare()
成員函數訪問。函數的返回類型滿足MutableBufferSequence
要求。 - 通過調用
commit()
成員函數,數據從輸出序列的前面傳輸到輸入序列的后面。 - 通過調用
consume()
成員函數從輸入序列的前面刪除數據。
streambuf 構造函數接受一個size_t
參數,指定輸入序列和輸出序列的大小之和的最大值。 如果成功,任何將內部數據增長超過此限制的操作都將拋出 std::length_error
異常。
按字節順序遍歷緩沖區序列
buffers_iterator<>
類模板允許遍歷緩沖區序列(即滿足 MutableBufferSequence
或ConstBufferSequence
要求的類型),就好像它們是連續的字節序列一樣。還提供了稱為 buffers_begin()
和 buffers_end()
的輔助函數,其中會自動推導出 buffers_iterator<>
模板參數。
舉個例子,從套接字中讀取一行放入std::string,可以寫為:
boost::asio::streambuf sb;
...
std::size_t n = boost::asio::read_until(sock,sb,'\n');
boost::asio::streambuf::const_buffers_type bufs = sb.data();
std::string line(
boost::asio::buffers_begin(bufs),
boost::asio::buffers_begin(bufs)+n
);
Buffer debugging
一些標准庫的實現,比如微軟Visual c++ 8.0及更高版本附帶的庫,提供了一個稱為迭代器調試的特性。這意味着在運行時檢查迭代器的有效性。如果程序嘗試使用已失效的迭代器,則會觸發斷言。 例如:
std::vector<int> v(1);
std::vector<int>::iterator i = v.begin();
v.clear(); //使迭代器無效
*i=0; //斷言
Asio利用了這一特性,加入到了buffer的debugging。考慮下面的代碼:
void dont_do_this() {
std::string msg = "Hello,world!";
boost::asio::async_write(sock,boost::asio::buffer(msg),my_handler);
}
當您調用異步讀取或寫入時,您需要確保操作的緩沖區在調用完成處理器之前有效。在上面的例子中,緩沖區是 std::string 變量 msg。這個變量在堆棧上,所以它在異步操作完成之前就超出了范圍。如果你很幸運,那么應用程序會崩潰,但更有可能出現隨機故障。
啟用緩沖區調試時,Asio 將迭代器存儲到string中,直到異步操作完成,然后解引用它以檢查其有效性。在上面的示例中,您將在 Asio 嘗試調用完成處理器之前觀察到斷言失敗。
當定義_GLIBCXX_DEBUG
時,此功能會自動適用於 Microsoft Visual Studio 8.0 或更高版本以及 GCC。此檢查會產生性能成本,因此緩沖區調試僅在調試版本中啟用。對於其他編譯器,它可以通過定義 BOOST_ASIO_ENABLE_BUFFER_DEBUGGING
來啟用。 它也可以通過定義 BOOST_ASIO_DISABLE_BUFFER_DEBUGGING
來顯式禁用。
Streams,Short Read and Short Writes
Asio的許多I/O對象是面向流。這就意味着:
- 沒有消息邊界。數據時作為連續的字節序列傳輸的。
- 讀或寫操作可能傳輸的字節比要求的更少。這被稱為短讀(short read)或短寫(short write)。
提供面向流的 I/O 模型的對象具有以下一種或多種類型要求:
SyncReadStream
:其中使用名為read_some()
的成員函數執行同步讀取操作。AsyncReadStream
,其中使用名為async_read_some()
的成員函數執行異步讀取操作。SyncWriteStream
,其中使用名為write_some()
的成員函數執行同步寫入操作。AsyncWriteStream
,其中使用名為async_write_some()
的成員函數執行異步寫入操作。
面向流的IO對象的例子包括ip::tcp::socket,ssl::stream<>,posix::stream_descriptor,windows::stream_handle
等等。
程序通常希望傳輸確切數量的字節。 當發生短讀或短寫時,程序必須重新開始操作,並繼續這樣做,直到傳輸了所需的字節數。 Asio 提供了自動執行此操作的通用函數:read()
、async_read()
、write()
和 async_write()
。
為什么EOF是錯誤
- 流的結尾會導致
read、async_read、read_until
或async_read_until
函數違反它們的約定。 例如。 由於 EOF,N 個字節的讀取可能會提前完成。 - EOF 錯誤可用於區分流的結束和成功讀取了0字節大小的數據。
Reactor風格的操作
有時,程序必須與想要自己執行 I/O 操作的第三方庫集成。為促進這一點,Asio 的同步和異步操作可用於等待套接字准備好讀取、准備寫入或具有掛起的錯誤條件。
舉個例子,執行非阻塞讀:
ip::tcp::socket socket(my_io_context);
...
socket.non_blocking(true);
...
socket.async_wait(ip::tcp::socket::wait_read,read_handler);
...
void read_handler(boost::system::error_code ec) {
if(!ec) {
std::vector<charA> buf(socket.available());
socket.read_some(buffer(buf));
}
}
所有平台上的套接字和 POSIX 面向流的描述符類都支持這些操作。
基於行的操作
許多常用的 Internet 協議都是基於行的,這意味着它們具有由字符序列“\r\n”分隔的協議元素。例如HTTP,SMTP,FTP。為了更容易地實現基於行的協議以及其他使用分隔符的協議,Asio 提供了包括read_until()
和 async_read_until()
的函數。
下面例子說明了async_read_until()
在HTTP服務器中的使用,用來接收來自客戶端的HTTP請求的第一行:
class http_connection {
...
void start() { boost::asio::async_read_until(socket_,data_,"\r\n",boost::bind(&http_connection::handle_request_line,this,_1));
}
void handle_request_line(boost::system::error_code ec) {
if(!ec) {
std::string method, uri, version;
char sp1,sp2,cr,lf;
std::istream is(&data_);
is.unsetf(std::ios_base::skipws);
is >> method >>sp1 >> uri >> sp2 >>version >> cr >> lf;
...
}
}
...
boost::asio::ip::tcp::socket socket_;
boost::asio::streambuf data_;
};
streambuf 數據成員用作存儲在搜索分隔符之前從套接字讀取的數據的地方。重要的是要記住,分隔符之后可能還有其他數據。 這個多余的數據應該留在流緩沖中,以便后續調用read_until()
或 async_read_until()
可以檢查它。
分隔符可以指定為單個char
、std::string
或 boost::regex
。 read_until()
和 async_read_until()
函數還包括接受稱為匹配條件的用戶定義函數對象的重載。 例如,要將數據讀入流緩沖直到遇到空格:
typedef boost::asio::buffers_iterator<boost::asio::streambuf::const_buffers_type> iterator;
std::pair<iterator,bool> match_whitespace(iterator begin,iterator end) {
iterator i = begin;
while(i!=end) {
if(std::isspace(*i++))
return std::make_pair(i,true);
return std::make_pair(i,false);
}
}
...
boost::asio::streambuf b;
boost::asio::read_until(s,b,match_whitespace);
將數據讀入流緩沖區直到找到匹配的字符為止。
class match_char
{
public:
explicit match_char(char c) : c_(c) {}
template <typename Iterator>
std::pair<Iterator,bool>operator()(Iterator begin,iterator end) const
{
Iterator i = begin;
while(i != end)
if(c_ == *i++)
return std::make_pair(i,true);
return std::make_pair(i,false);
}
private:
char c_;
};
namespace boost
{
namespace asio
{
template<> struct is_match_condition<match_char> : public boost::true_type {}
}
}
....
boost::asio::streambuf b;
boost::asio::read_until(s,b,match_char('a'));
對於函數和具有嵌套 result_type 類型定義的函數對象, is_match_condition<>
類型特征自動計算為真。對於其他類型,特征必須明確特化,如上所示。
自定義內存分配器
許多同步操作需要申請一個對象來存儲與操作相關的狀態。例如,Win32 實現需要將 OVERLAPPED 派生對象傳遞給 Win32 API 函數。
並且,程序通常包含易於識別的同步操作鏈。半雙工協議實現(例如 HTTP 服務器)每個客戶端都有一個操作鏈(接收后發送)。一個全雙工協議實現有兩條並行執行的鏈。程序應該能夠利用這些知識為鏈中的所有異步操作重用內存。
給定一個用戶定義的Handler對象h的拷貝,如果實現需要分配與該Handler關聯的內存,它將使用 get_related_allocator
函數獲取分配器。例如:
boost::asio::associated_allocator_t<Handler> a = boost::asio::get_associated_allocator(h);
關聯的分配器必須滿足標准的分配器的標准。
默認情況下,處理器使用標准分配器(用::oprator new()
和::operator delete()
實現)。可以通過指定嵌套類型 allocator_type 和成員函數 get_allocator() 為特定處理其類型定制分配器:
class my_handler
{
public:
// 分配器類型要求的自定義實現.
typedef my_allocator allocator_type;
// 返回一個自定義的分配器實現
allocator_type get_allocator() const noexcept
{
return my_allocator();
}
void operator()() { ... }
};
在大多數復雜長江下,會直接對associated_allocator
模板進行特化:
namespace boost
{
namespace asio
{
template <typename Allocator>
struct associated_allocator<my_handler, Allocator>
{
// Custom implementation of Allocator type requirements.
typedef my_allocator type;
// Return a custom allocator implementation.
static type get(const my_handler&,
const Allocator& a = Allocator()) noexcept
{
return my_allocator();
}
};
}
} // namespace boost::asio
該實現保證釋放將在調用關聯的處理器之前發生,這意味着內存已准備好重新用於處理器啟動的任何新異步操作。
可以從調用庫函數的任何用戶創建的線程調用自定義內存分配函數。 該實現保證,對於包含在庫中的異步操作,該實現不會對該處理器的內存分配函數進行並發調用。 如果需要從不同線程調用分配函數,在實現時將插入適當的內存屏障以確保正確的內存可見性。
Handler追蹤
為了幫助調試異步程序,Asio提供了對Handler追蹤的支持。通過定義BOOST_ASIO_ENABLE_HANDLER_TRACKING
開啟,Asio將調試輸出寫入標准錯誤流。輸出記錄異步操作及其和Handler之間的關系。
此功能在調試時很有用,您需要知道異步操作是如何鏈接在一起的,或者掛起的異步操作是什么。下面是運行HTTP服務器示例時的輸出,處理單個請求,然后通過Ctrl+C關閉:
@asio|1589424178.741850|0*1|signal_set@0x7ffee977d878.async_wait
@asio|1589424178.742593|0*2|socket@0x7ffee977d8a8.async_accept
@asio|1589424178.742619|.2|non_blocking_accept,ec=asio.system:11
@asio|1589424178.742625|0|resolver@0x7ffee977d760.cancel
@asio|1589424195.830382|.2|non_blocking_accept,ec=system:0
@asio|1589424195.830413|>2|ec=system:0
@asio|1589424195.830473|2*3|socket@0x7fa71d808230.async_receive
@asio|1589424195.830496|.3|non_blocking_recv,ec=system:0,bytes_transferred=151
@asio|1589424195.830503|2*4|socket@0x7ffee977d8a8.async_accept
@asio|1589424195.830507|.4|non_blocking_accept,ec=asio.system:11
@asio|1589424195.830510|<2|
@asio|1589424195.830529|>3|ec=system:0,bytes_transferred=151
@asio|1589424195.831143|3^5|in 'async_write' (./../../../boost/asio/impl/write.hpp:330)
@asio|1589424195.831143|3*5|socket@0x7fa71d808230.async_send
@asio|1589424195.831186|.5|non_blocking_send,ec=system:0,bytes_transferred=1090
@asio|1589424195.831194|<3|
@asio|1589424195.831218|>5|ec=system:0,bytes_transferred=1090
@asio|1589424195.831263|5|socket@0x7fa71d808230.close
@asio|1589424195.831298|<5|
@asio|1589424199.793770|>1|ec=system:0,signal_number=2
@asio|1589424199.793781|1|socket@0x7ffee977d8a8.close
@asio|1589424199.793809|<1|
@asio|1589424199.793840|>4|ec=asio.system:125
@asio|1589424199.793854|<4|
@asio|1589424199.793883|0|signal_set@0x7ffee977d878.cancel
每一行的格式如下:
<tag> | <timestamp> | <action> | <description>
<tag>總是@asio,用於從程序輸出中識別和提取Handler追蹤消息。
<timestamp>是距離1970.1.1的秒和毫秒。
<action>采取下面的形式之一:
- >n:程序進入了編號n的處理器。<description>顯示Handler的參數。
- <n:程序離開了編號為n的處理器。
- !n:由於異常,程序離開的編號為n的處理器。
- ~n:編號為n的處理程序沒有被調用就被銷毀了。當
io_context
被銷毀時,任何未完成的異步操作通常都是這種情況。 - n^m:編號為n的處理程序將要創建一個新的異步操作,其完成處理器編號為m。<description>包含源位置信息,以幫助確定異步操作在程序中的何處啟動。
- n*m:編號為n的處理程序創建了一個新的異步操作,其完成處理程序編號為m。<description>顯示了啟動了哪些異步操作。
- n:編號為n的處理器執行了一些其他操作。<description>顯示了調用了什么函數。目前只有
close()
和cancel()
操作會被記錄,因為這些操作可能會影響掛起的異步操作的狀態。 - .n:該實現執行了一個系統調用,作為異步操作的一部分,完成處理器編號為n。 <description> 顯示調用了什么函數及其結果。 這些跟蹤事件僅在使用基於Reactor的實現時才會發出。
<description> 顯示同步或異步操作,格式為 <object-type>@<pointer>.<operation>。 對於處理程序條目,它顯示了一個逗號分隔的參數列表及其值。
如上所示,每個處理程序都分配了一個數字標識符。 如果處理器跟蹤輸出顯示處理程序編號為 0,則表示該操作是在任何處理器之外執行的。
添加局部信息
程序可以通過在源代碼中使用宏 BOOST_ASIO_HANDLER_LOCATION
來增加處理器跟蹤輸出的位置信息。 例如:
#define HANDLER_LOCATION \
BOOST_ASIO_HANDLER_LOCATION((__FILE__, __LINE__, __func__))
// ...
void do_read()
{
HANDLER_LOCATION;
auto self(shared_from_this());
socket_.async_read_some(boost::asio::buffer(data_, max_length),
[this, self](boost::system::error_code ec, std::size_t length)
{
HANDLER_LOCATION;
if (!ec)
{
do_write(length);
}
});
}
使用附加位置信息可用時,處理程序跟蹤輸出可能包括源位置的調用堆棧:
@asio|1589423304.861944|>7|ec=system:0,bytes_transferred=5
@asio|1589423304.861952|7^8|in 'async_write' (./../../../boost/asio/impl/write.hpp:330)
@asio|1589423304.861952|7^8|called from 'do_write' (handler_tracking/async_tcp_echo_server.cpp:62)
@asio|1589423304.861952|7^8|called from 'operator()' (handler_tracking/async_tcp_echo_server.cpp:51)
@asio|1589423304.861952|7*8|socket@0x7ff61c008230.async_send
@asio|1589423304.861975|.8|non_blocking_send,ec=system:0,bytes_transferred=5
@asio|1589423304.861980|<7|
此外,如果 std::source_location
或 std::experimental::source_location
可用,則 use_awaitable_t
標記(當默認構造或用作默認完成標記時)還將導致處理器跟蹤為每個新創建的異步操作輸出源位置 。 use_awaitable_t
對象也可以用局部信息顯式構造。
可視化展示
可以使用包含的 handlerviz.pl 工具對處理器跟蹤輸出進行后處理,以創建處理程序的可視化表示(需要 GraphViz 工具dot
)。
自定義追蹤
可以通過將 BOOST_ASIO_CUSTOM_HANDLER_TRACKING
宏定義為頭文件的名稱(用“”或 <> 括起來)來自定義處理器跟蹤。 此頭文件必須實現以下預處理器宏:
Macro | Description |
---|---|
BOOST_ASIO_INHERIT_TRACKED_HANDLER |
為實現異步操作的類指定基類。 使用時,宏緊跟在類名之后,因此它必須具有以下形式:public my_class。 |
BOOST_ASIO_ALSO_INHERIT_TRACKED_HANDLER |
為實現異步操作的類指定基類。 使用時,宏跟隨其他基類,因此它必須具有形式,public my_class。 |
BOOST_ASIO_HANDLER_TRACKING_INIT(args) |
用於初始化跟蹤機制的表達式。 |
BOOST_ASIO_HANDLER_LOCATION(args) |
用於定義源代碼位置的變量聲明。 args 是一個帶括號的函數參數列表,包含文件名、行號和函數名。 |
BOOST_ASIO_HANDLER_CREATION(args) |
在創建異步操作時調用的N表達式。Args是一個帶圓括號的函數參數列表,包含擁有的執行上下文、被跟蹤的處理程序、對象類型的名稱、對象的指針、對象的本機句柄和操作名稱。 |
BOOST_ASIO_HANDLER_COMPLETION(args) |
在異步操作完成時調用的表達式。 args 是包含跟蹤處理程序的帶括號的函數參數列表。 |
BOOST_ASIO_HANDLER_INVOCATION_BEGIN(args) |
在調用完成處理程序之前立即調用的表達式。 args 是一個帶括號的函數參數列表,包含完成處理程序的參數。 |
BOOST_ASIO_HANDLER_INVOCATION_END |
在調用完成處理程序后立即調用的表達式。 |
BOOST_ASIO_HANDLER_OPERATION(args) |
在調用某些同步對象操作(例如 close() 或 cancel())時調用的表達式。 args 是一個帶括號的函數參數列表,包含擁有的執行上下文、對象類型的名稱、指向對象的指針、對象的本機句柄和操作名稱。 |
BOOST_ASIO_HANDLER_REACTOR_REGISTRATION(args) |
當對象注冊到反應器時調用的表達式。 args 是一個帶括號的函數參數列表,包含擁有的執行上下文、對象的本機句柄和唯一的注冊鍵。 |
BOOST_ASIO_HANDLER_REACTOR_DEREGISTRATION(args) |
當對象從反應器中注銷時調用的表達式。 args 是一個帶括號的函數參數列表,包含擁有的執行上下文、對象的本機句柄和唯一的注冊鍵。 |
BOOST_ASIO_HANDLER_REACTOR_READ_EVENT |
用於識別反應器讀取就緒事件的位掩碼常量。 |
BOOST_ASIO_HANDLER_REACTOR_WRITE_EVENT |
用於標識反應器寫入准備事件的位掩碼常量。 |
BOOST_ASIO_HANDLER_REACTOR_ERROR_EVENT |
用於識別反應器錯誤准備事件的位掩碼常量。 |
BOOST_ASIO_HANDLER_REACTOR_EVENTS(args) |
當注冊到反應器的對象准備就緒時調用的表達式。 args 是一個帶括號的函數參數列表,包含擁有的執行上下文、唯一的注冊鍵和就緒事件的位掩碼。 |
BOOST_ASIO_HANDLER_REACTOR_OPERATION(args) |
當實現作為基於反應器的異步操作的一部分執行系統調用時調用的表達式。 args 是一個帶括號的函數參數列表,包含被跟蹤的處理程序、操作名稱、操作產生的錯誤代碼和(可選)傳輸的字節數。 |
並發提示
io_context
構造器允許程序指定一個並發提示。這是對io_context
實現中應用於運行完成處理器的活動線程數的建議。
當后台使用 Windows I/O 完成端口時,此值將傳遞給 CreateIoCompletionPort
。
當使用基於Reactor的后端時,實現會識別以下特殊的並發提示值:
Value | Description |
---|---|
1 |
該實現假設 io_context 將從單個線程運行,並基於此假設應用多項優化。例如,當一個處理程序從另一個處理程序中發布時,新的處理程序被添加到一個快速線程本地隊列(結果是新的處理程序被阻止,直到當前正在執行的處理程序完成)。 |
BOOST_ASIO_CONCURRENCY_HINT_UNSAFE |
這個特殊的並發提示禁用了調度程序和反應器 I/O 中的鎖定。 |
BOOST_ASIO_CONCURRENCY_HINT_UNSAFE_IO |
這個特殊的並發提示禁用反應器 I/O 中的鎖定。 |
BOOST_ASIO_CONCURRENCY_HINT_SAFE |
默認值。io_context提供了完整的線程安全性,並且任何線程都可以使用不同的I/O對象。 |
通過定義BOOST_ASIO_CONCURRENCY_HINT_DEFAULT
宏,可以在編譯時覆蓋默認構造的 io_context
對象使用的並發提示。 例如,在編譯期命令行指定
-DBOOST_ASIO_CONCURRENCY_HINT_DEFAULT=1
意味着對程序中所有默認構造的 io_context 對象使用並發提示 1。類似地,可以通過定義 BOOST_ASIO_CONCURRENCY_HINT_1
來覆蓋由 1 構造的 io_context 對象使用的並發提示。 例如,傳遞
-DBOOST_ASIO_CONCURRENCY_HINT_1=BOOST_ASIO_CONCURRENCY_HINT_UNSAFE
給編譯期會禁用所有對象的線程安全。
無堆棧協程
coroutine
類提供無堆棧協程的支持。無堆棧協程使程序能夠以最小的開銷以同步方式實現異步邏輯,如下例所示:
struct session : boost::asio::coroutine
{
boost::shared_ptr<tcp::socket> socket_;
boost::shared_ptr<std::vector<char> > buffer_;
session(boost::shared_ptr<tcp::socket> socket)
: socket_(socket),
buffer_(new std::vector<char>(1024))
{
}
void operator()(boost::system::error_code ec = boost::system::error_code(), std::size_t n = 0)
{
if (!ec) reenter (this)
{
for (;;)
{
yield socket_->async_read_some(boost::asio::buffer(*buffer_), *this);
yield boost::asio::async_write(*socket_, boost::asio::buffer(*buffer_, n), *this);
}
}
}
};
coroutine
類與偽關鍵字reenter,yield,fork
同時使用。它們是預處理器宏,並使用類似於 Duff's Device 的技術根據 switch 語句實現。 coroutine
類的文檔提供了這些偽關鍵字的完整描述。
堆棧式協程
spawn()
函數是用於運行堆棧協程的高級包裝器。 它基於 Boost.Coroutine 庫。 spawn()
函數使程序能夠以同步方式實現異步邏輯,如下例所示:
boost::asio::spawn(my_strand, do_echo);
// ...
void do_echo(boost::asio::yield_context yield)
{
try
{
char data[128];
for (;;)
{
std::size_t length =
my_socket.async_read_some(
boost::asio::buffer(data), yield);
boost::asio::async_write(my_socket,
boost::asio::buffer(data, length), yield);
}
}
catch (std::exception& e)
{
// ...
}
}
spawn() 的第一個參數可能是一個strand、io_context
或完成處理器。 此參數確定允許協程執行的上下文。 例如,服務器的每個客戶端對象可能由多個協程組成; 它們都應該在同一strand上運行,這樣就不需要顯式同步。
第二個參數是一個帶有簽名的函數對象,說明指定的代碼作為協程的一部分運行:
void coroutine(boost::asio::yield_context yield);
參數 yield 可以傳遞給異步操作來代替完成處理器,如下所示:
std::size_t length = my_socket.async_read_some(boost::asio::buffer(data),yield);
這將啟動異步操作並暫停協程。 異步操作完成后,協程將自動恢復。
其中異步操作的處理程序簽名具有以下形式,啟動函數返回 result_type。:
void handler(boost::system::error_code ec, result_type result);
在上面的 async_read_some
示例中,這是 size_t。 如果異步操作失敗,則將error_code
轉換為system_error
異常並拋出。
處理器簽名如下形式,啟動函數返回void:
void handler(boost::system::error_code ec);
如上所述,錯誤作為system_error
異常傳遞回協程。
要從操作中收集 error_code,而不是讓它拋出異常,請將輸出變量與 yield_context
關聯,如下所示:
boost::system::error_code ec;
std::size_t length =
my_socket.async_read_some(
boost::asio::buffer(data), yield[ec]);
注意:如果 spawn()
與 Handler 類型的自定義完成處理程序一起使用,則函數對象簽名實際上是:
void coroutine(boost::asio::basic_yield_context<Handler> yield);
協程TS支持
通過 awaitable
類模板、use_awaitable
完成標記和co_spawn()
函數提供對 Coroutines TS 的支持。 這些工具允許程序以同步方式實現異步邏輯,結合 co_await 關鍵字,如以下示例所示:
boost::asio::co_spawn(executor, echo(std::move(socket)), boost::asio::detached);
// ...
boost::asio::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), boost::asio::use_awaitable);
co_await async_write(socket, boost::asio::buffer(data, n), boost::asio::use_awaitable);
}
}
catch (std::exception& e)
{
std::printf("echo Exception: %s\n", e.what());
}
}
co_spawn()
的第一個參數是一個執行器,它確定允許協程執行的上下文。 例如,服務器的每個客戶端對象可能由多個協程組成; 它們都應該在同一個strand上運行,這樣就不需要顯式同步。
第二個參數是一個awaitable<R>
,它是協程入口點函數的返回結果,在上面的例子中是調用echo的結果。 (或者,此參數可以是返回 awaitable<R>
的函數對象。)模板參數 R 是協程生成的返回值的類型。 在上面的例子中,協程返回 void。
第三個參數是一個完成標記,co_spawn()
使用它來生成一個帶有簽名 void(std::exception_ptr, R)
的完成處理程序。 一旦完成,這個完成處理程序就會被協程的結果調用。 在上面的示例中,我們傳遞了一個完成標記類型 boost::asio::detached
,它用於顯式忽略異步操作的結果。
在這個例子中,協程的主體是在 echo 函數中實現的。 當 use_awaitable
完成令牌傳遞給異步操作時,此異步操作的啟動函數返回一個可與co_await
關鍵字一起使用的可等待對象:
std::size_t n = co_await socket.async_read_some(boost::asio::buffer(data), boost::asio::use_awaitable);
其中異步操作的處理程序函數簽名具有以下形式:
void handler(boost::system::error_code ec, result_type result);
co_await
表達式的結果類型是 result_type。在上面的 async_read_some
示例中,這是 size_t
。 如果異步操作失敗,則將error_code
轉換為system_error
異常並拋出。
處理程序函數簽名為如下形式的:
void handler(boost::system::error_code ec);
co_await
表達式產生一個 void 結果。 如上所述,錯誤作為 system_error
異常傳遞回協程。