作者:謝敬偉,江湖人稱“刀哥”,20年IT老兵,數(shù)據(jù)通信網(wǎng)絡(luò)專家,電信網(wǎng)絡(luò)架構(gòu)師,目前任Netwarps開發(fā)總監(jiān)。刀哥在操作系統(tǒng)、網(wǎng)絡(luò)編程、高并發(fā)、高吞吐、高可用性等領(lǐng)域有多年的實(shí)踐經(jīng)驗(yàn),并對(duì)網(wǎng)絡(luò)及編程等方面的新技術(shù)有濃厚的興趣。
Rust歷史不長(zhǎng),仍然處于快速發(fā)展的歷程中。關(guān)于異步編程的模式,現(xiàn)在已經(jīng)發(fā)展到async/await協(xié)程的高級(jí)階段。大概是因?yàn)?code>async/await出現(xiàn)的時(shí)間還不長(zhǎng),所以現(xiàn)有大多數(shù)的開源項(xiàng)目并不是或不是純粹使用async/await來(lái)書寫的,而是前前后后有多種的寫法。這樣的狀況給Rust的學(xué)習(xí)帶來(lái)了一些的難度。在這里,我們來(lái)捋一捋異步代碼的幾種寫法。
mio
最原始的方式是使用mio進(jìn)行開發(fā)。mio是一個(gè)底層異步I/O庫(kù),提供非阻塞方式的API,具有很高的性能。實(shí)際上mio是對(duì)于操作系統(tǒng)epoll/kqueue/IOCP的封裝。在C/C++中我們使用libevent之類的庫(kù),mio可以理解為對(duì)應(yīng)的Rust版本?;?code>mio的代碼大致如下:
loop {
// Poll Mio for events, blocking until we get an event.
poll.poll(&mut events, None)?;
// Process each event.
for event in events.iter() {
if event.is_writable() {
// socket可寫,開始發(fā)送數(shù)據(jù)
}
if event.is_readable() {
// socket可讀,開始接收數(shù)據(jù)
}
// socket 關(guān)閉,退出循環(huán)
return Ok(());
}
}
總的來(lái)說(shuō),這是完全基于異步事件通知的寫法,和C/C++區(qū)別不是很大,異步代碼對(duì)于程序員是一個(gè)挑戰(zhàn),當(dāng)代碼邏輯越來(lái)越復(fù)雜,添加新功能或是解決已有問(wèn)題的難度也越來(lái)越大。
另外,mio實(shí)現(xiàn)的是一個(gè)單線程事件循環(huán),雖然可以處理成千上萬(wàn)路的I/O操作,但沒有多線程的能力,需要自己擴(kuò)充。
Future Poll
為了更好地規(guī)范異步的邏輯,Rust抽象出Future表示尚未發(fā)生的事物。這些Future可以用很多方式組合成一個(gè)更復(fù)雜的復(fù)合Future來(lái)代表一系列的事件。Future需要程序主動(dòng)去poll(輪詢)才能獲取到最終的結(jié)果,每一次輪詢的結(jié)果可能是Ready或者Pending。
運(yùn)行庫(kù)提供Executor和Reactor來(lái)執(zhí)行Future,也就是調(diào)用Future的poll方法循環(huán)執(zhí)行一系列就緒的Future,當(dāng)Future返回Pending的時(shí)候,會(huì)將Future轉(zhuǎn)移到Reactor上等待喚醒。Reactor被用來(lái)負(fù)責(zé)喚醒之前無(wú)法完成的Future。事實(shí)上,tokio的Reactor是基于mio實(shí)現(xiàn)的,而async-std/smol則是封裝了epoll/kqueue/IOCP,提供類似的功能。
手動(dòng)實(shí)現(xiàn)Future是一件相對(duì)繁瑣的工作,主要的問(wèn)題在于異步模式本身的特性。例如,接收網(wǎng)絡(luò)數(shù)據(jù),無(wú)法臆測(cè)每次輪詢會(huì)收到多少字節(jié)的數(shù)據(jù),往往需要開辟一段接收緩沖區(qū)容納數(shù)據(jù),協(xié)議解碼也需要一個(gè)狀態(tài)機(jī)拼包向上層提交;發(fā)送網(wǎng)絡(luò)數(shù)據(jù)存在相似問(wèn)題,發(fā)送數(shù)據(jù)時(shí)底層未就緒,則緩沖發(fā)送數(shù)據(jù),待下次輪詢時(shí),需要首先檢查并處理發(fā)送緩沖區(qū)。另外還有一些值得注意的地方,如果手動(dòng)實(shí)現(xiàn)的Future返回Pending,則必須自己實(shí)現(xiàn)喚醒機(jī)制,也就是需要將cx克隆一份記下來(lái),然后在適當(dāng)?shù)臅r(shí)侯調(diào)用cx.wake()。因?yàn)榫W(wǎng)絡(luò)相關(guān)的功能往往是分層的,因此手動(dòng)的Poll循環(huán)也會(huì)是層層堆疊的,這時(shí)候,返回值Poll::Ready(T)就有學(xué)問(wèn)了。泛型T可能包裹各種不同的數(shù)據(jù),Option<T>,Result<T,E>,或者兩者的組合。因?yàn)樽钔鈱舆€有一個(gè)Poll<T>,所有這時(shí)候的match語(yǔ)句寫起來(lái)會(huì)非常臃腫,粘貼復(fù)制寫很多代碼,完成的功能卻非常有限,而且由于這些代碼很相似,大大增加了出錯(cuò)的可能性。
標(biāo)準(zhǔn)庫(kù)中僅僅定義了Future,更多的相關(guān)功能需要引用futures-rs類庫(kù),里面定義了一系列有關(guān)異步的操作,包括Stream、Sink、AsyncRead、AsyncWrite等基礎(chǔ)Trait,以及對(duì)應(yīng)實(shí)現(xiàn)了大量方便操作的組合子的Ext Trait,特別用途的fused、Box,Try系列的擴(kuò)展,諸如join!、select!、pin_mut!等一系列的宏。理論上,不使用這些擴(kuò)展也能寫出代碼,只不過(guò)那樣的代碼很可能篇幅會(huì)長(zhǎng)的可怕。值得一提的是,除了一些可以簡(jiǎn)化代碼的過(guò)程宏之外,擴(kuò)展Trait提供的組合子也會(huì)讓代碼精簡(jiǎn)不少。比如Future::and_then可以讓代碼寫成鏈?zhǔn)秸{(diào)用的方式;Sink::send包裝了Sink發(fā)送三步驟 poll_ready/start_send/poll_flush,使用.await一行代碼直接就可以完成發(fā)送。因此,很多poll方式的代碼實(shí)際上是準(zhǔn)確地說(shuō)是混合式的,其中也使用了不少async代碼塊。
總之,搞清楚Future相關(guān)的這些內(nèi)容是需要花費(fèi)不少時(shí)間,更不用說(shuō)用它們來(lái)寫代碼了。不過(guò),即便是使用async/await這種更高級(jí)原語(yǔ),也是有必要了解底層的工作原理和實(shí)現(xiàn)機(jī)制,所謂知其然知其所以然。
async/await
使用async/await可以將異步的代碼寫得類似同步的過(guò)程,更加符合人體工程學(xué)。因?yàn)?code>async被翻譯為一個(gè)Future狀態(tài)機(jī),原先在poll方式中需要處理的與Pending相關(guān)的狀態(tài)現(xiàn)在都由async生成的狀態(tài)機(jī)自動(dòng)完成,因此大大減輕了程序員的心智負(fù)擔(dān)。
如前所述,底層的Futures提供了很多方便的組合子擴(kuò)展Future,使用起來(lái)很簡(jiǎn)潔,可以極大地簡(jiǎn)化代碼。例如,上文提到過(guò)的Sink::send包裝了發(fā)送緩沖區(qū)的實(shí)現(xiàn)和異步發(fā)送的三個(gè)步驟;AsyncRead::read_exact實(shí)現(xiàn)了讀取指定字節(jié)數(shù)的功能,在處理網(wǎng)絡(luò)協(xié)議解析時(shí)可以避免手寫一個(gè)拼包狀態(tài)機(jī);AsyncWrite::write_all實(shí)現(xiàn)了發(fā)送全部數(shù)據(jù)以及發(fā)送緩沖,等等。正是在這些底層功能的支持下,async/await成為了更高級(jí)的書寫異步代碼的方式。也許會(huì)有少許擔(dān)心,這樣所謂“高級(jí)”會(huì)不會(huì)在性能上有很大損失?筆者個(gè)人不這么認(rèn)為。自動(dòng)實(shí)現(xiàn)的狀態(tài)機(jī)也許未必比程序員手動(dòng)完成的性能更差。狀態(tài)機(jī)編程對(duì)于任何人,即便是一個(gè)有經(jīng)驗(yàn)的程序員都是不小挑戰(zhàn)。蹩腳的狀態(tài)機(jī)實(shí)現(xiàn)不僅可能有性能問(wèn)題,更大的風(fēng)險(xiǎn)來(lái)自于實(shí)現(xiàn)上的漏洞,以及維護(hù)上的困難。代碼寫出來(lái)更多是給別人看的,完成同樣的功能,簡(jiǎn)潔的代碼更有可能是更高質(zhì)量的代碼。
以下例子是固定長(zhǎng)度分割的報(bào)文接收過(guò)程,使用async/await是很簡(jiǎn)單的。如果實(shí)現(xiàn)為一個(gè)Stream/poll_next,代碼會(huì)復(fù)雜很多。
/// convenient method for reading a whole frame
pub async fn recv_frame(&mut self) -> io::Result<Vec<u8>> {
let mut len = [0; 4];
let _ = self.inner.read_exact(&mut len).await?; // inner socket, 支持 AsyncRead
let n = u32::from_be_bytes(len) as usize;
if n > self.max_frame_len {
let msg = format!(
"data length {} exceeds allowed maximum {}",
n, self.max_frame_len
);
return Err(io::Error::new(io::ErrorKind::PermissionDenied, msg));
}
let mut frame = vec![0; n];
self.inner.read_exact(&mut frame).await?;
Ok(frame)
}
最后,完全使用async/await寫代碼目前還有幾個(gè)問(wèn)題:
-
async trait
當(dāng)前Trait 不支持 async fn,無(wú)法直接用Trait來(lái)抽象異步方法。暫時(shí)解決辦法是使用三方庫(kù) async-trait。如下:
use async_trait::async_trait;
#[async_trait]
trait Advertisement {
async fn run(&self);
}
宏 async_trait將代碼轉(zhuǎn)換為一個(gè)返回 Pin<Box<dyn Future + Send + 'async>> 的同步方法。因?yàn)檠b箱和動(dòng)態(tài)派發(fā)的原因,性能上會(huì)有少許損失。
-
異步析構(gòu)
當(dāng)前drop方法必須是同步調(diào)用,不能使用await語(yǔ)法。當(dāng)一個(gè)I/O對(duì)象越過(guò)生命周期被析構(gòu),往往在關(guān)閉底層句柄之前,還需要完成某些I/O操作。比如,通知網(wǎng)絡(luò)對(duì)端連接已經(jīng)關(guān)閉。在同步代碼中,我們只需要在drop()中置入這些操作,但是在異步代碼中,無(wú)法在drop()中做類似的事情。
解決辦法,總是在異步I/O對(duì)象越過(guò)生命周期之前顯式地執(zhí)行關(guān)閉動(dòng)作,或是,實(shí)現(xiàn)一個(gè)類似GC的功能,專門負(fù)責(zé)清理工作。
展望
筆者在學(xué)習(xí)Rust過(guò)程中,主要關(guān)注網(wǎng)絡(luò)相關(guān)的并發(fā)編程。因?yàn)橹坝性?code>Go版本的ipfs/libp2p上的開發(fā)經(jīng)驗(yàn),故而學(xué)習(xí)研究了rust-libp2p以及nervos tentacle。rust-libp2p是Parity實(shí)現(xiàn)的準(zhǔn)官方版本,但是這個(gè)項(xiàng)目的代碼及其難懂,過(guò)于強(qiáng)調(diào)使用泛型參數(shù)的抽象,導(dǎo)致代碼可讀性非常差。請(qǐng)教了代碼作者,他承認(rèn)代碼可能有些復(fù)雜,但也強(qiáng)調(diào)都是有原因的... nervos tentacle的實(shí)現(xiàn)在協(xié)議上不夠完整,特別是與標(biāo)準(zhǔn)libp2p并不兼容。兩個(gè)項(xiàng)目共有的特點(diǎn)是主要用poll的方式寫代碼,邏輯上都是狀態(tài)機(jī)的嵌套。
因此,筆者試圖完全使用async/await方式重構(gòu)libp2p,參考rust-libp2p的實(shí)現(xiàn),代碼協(xié)程化,向上層提供純粹的異步接口,爭(zhēng)取在API層面的體驗(yàn)接近go-libp2p,這是推廣Rust協(xié)程機(jī)制的一個(gè)嘗試,同時(shí)也是個(gè)人的一個(gè)學(xué)習(xí)的過(guò)程。目前剛剛起步,僅完成了secio與yamux部分,待合適時(shí)機(jī)開源,期望更多Rust愛好者共同來(lái)開發(fā)完善。
深圳星鏈網(wǎng)科科技有限公司(Netwarps),專注于互聯(lián)網(wǎng)安全存儲(chǔ)領(lǐng)域技術(shù)的研發(fā)與應(yīng)用,是先進(jìn)的安全存儲(chǔ)基礎(chǔ)設(shè)施提供商,主要產(chǎn)品有去中心化文件系統(tǒng)(DFS)、企業(yè)聯(lián)盟鏈平臺(tái)(EAC)、區(qū)塊鏈操作系統(tǒng)(BOS)。
微信公眾號(hào):Netwarps