Dart與消息循環機制
翻譯自https://www.dartlang.org/articles/event-loop/
異步任務在Dart中隨處可見,例如許多庫的方法調用都會返回Future對象來實現異步處理,我們也可以注冊Handler來響應一些事件,如:鼠標點擊事件,I/O流結束和定時器到期。
這篇文章主要介紹了Dart中與異步任務相關的消息循環機制,閱讀完這篇文章后相信你可寫出更贊的異步執行代碼。你也能學習到如何調度Future任務並且預測他們的執行順序。
在閱讀這篇文章之前,你最好先要了解一下基本的Future用法。
基本概念
如果你寫過一些關於UI的代碼,你就應該熟悉消息循環和消息隊列。有了他們才能保重UI的繪制操作和一些UI事件,如鼠標點擊事件可以被一個一個的執行從而保證UI和UI事件的統一性。
消息循環和消息隊列
一個消息循環的職責就是不斷從消息隊列中取出消息並處理他們直到消息隊列為空。
消息隊列中的消息可能來自用戶輸入,文件I/O消息,定時器等。例如下圖的消息隊列就包含了定時器消息和用戶輸入消息。
上述的這些概念你可能已經駕輕就熟了,那接下來我們就討論一下這些概念在Dart中是怎么表現的?
Dart的單線程執行
當一個Dart的方法開始執行時,他會一直執行直至達到這個方法的退出點。換句話說Dart的方法是不會被其他Dart代碼打斷的。
Note:一個Dart的命令行應用可以通過創建isolates來達到並行運行的目的。isolates之間不會共享內存,它們就像幾個運行在不同進程中的app,中能通過傳遞message來進行交流。出了明確指出運行在額外的isolates或者workers中的代碼外,所有的應用代碼都是運行在應用的main isolate中。要了解更多相關內容,可以查看https://www.dartlang.org/articles/event-loop/#use-isolates-or-workers-if-necessary
正如下圖所示,當一個Dart應用開始的標志是它的main isolate執行了main方法。當main方法退出后,main isolate的線程就會去逐一處理消息隊列中的消息。
事實上,上圖是經過簡化的流程。
Dart的消息循環和消息隊列
一個Dart應用有一個消息循環和兩個消息隊列-- event隊列和microtask隊列。
event隊列包含所有外來的事件:I/O,mouse events,drawing events,timers,isolate之間的message等。
microtask 隊列在Dart中是必要的,因為有時候事件處理想要在稍后完成一些任務但又希望是在執行下一個事件消息之前。
event隊列包含Dart和來自系統其它位置的事件。但microtask隊列只包含來自當前isolate的內部代碼。
正如下面的流程圖,當main方法退出后,event循環就開始它的工作。首先它會以FIFO的順序執行micro task,當所有micro task執行完后它會從event 隊列中取事件並執行。如此反復,直到兩個隊列都為空。
注意:當事件循環正在處理micro task的時候。event隊列會被堵塞。這時候app就無法進行UI繪制,響應鼠標事件和I/O等事件
雖然你可以預測任務執行的順序,但你無法准確的預測到事件循環何時會處理你期望的任務。例如當你創建一個延時1s的任務,但在排在你之前的任務結束前事件循環是不會處理這個延時任務的,也就是或任務執行可能是大於1s的。
通過鏈接的方式指定任務順序
如果你的代碼之間存在依賴,那么盡量讓他們之間的依賴關系明確一點。明確的依賴關系可以很好的幫助其他開發者理解你的代碼,並且可以讓你的代碼更穩定也更容易重構。
先來看看下面這段錯誤代碼:
// 這樣寫錯誤的原因就是沒有明確體現出設置變量和使用變量之間的依賴關系
future.then(...set an important variable...);
Timer.run(() {...use the important variable...});
正確的寫法應該是:
// 明確表現出了后者依賴前者設置的變量值
future.then(...set an important variable...)
.then((_) {...use the important variable...});
為了表示明確的前后依賴關系,我們可以使用then()()來表明要使用變量就必須要等設置完這個變量。這里可以使用whenComplete()來代替then,它與then的不同點在於哪怕設置變量出現了異常也會被調用到。這個有點像java中的finally。
如果上面這個使用變量也要花費一段時間,那么可以考慮將其放入一個新的Future中:
future.then(...set an important variable...)
.then((_) {new Future(() {...use the important variable...})});
使用一個新的Future可以給事件循環一個機會先去處理列隊中的其他事件。
怎么安排一個任務
當你需要指定一些代碼稍后運行的時候,你可以使用dart:async提供的兩種方式:
1.Future類,它可以向event隊列的尾部添加一個事件。
2.使用頂級方法**scheduleMicrotask()**,它可以向microtask隊列的尾部添加一個微任務。
使用合理的隊列
有可能的還是盡量使用Future來向event隊列添加事件。使用event隊列可以保持microtask隊列的簡短,以此減少microtask的過度使用導致event隊列的堵塞。
如果一個任務確實要在event隊列的任何一個事件前完成,那么你應該盡量直接寫在main方法中而不是使用這兩個隊列。如果你不能那么就用scheduleMicrotask來向microtask添加一個微任務。
Event隊列
使用new Future
或者new Future.delayed()
來向event隊列中添加事件。
注意:你也可以使用
Timer
來安排任務,但是使用Timer的過程中如果出現異常,則會退出程序。這里推薦使用Future,它是構建在Timer之上並加入了更多的功能,比如檢測任務是否完成和異常反饋。
立刻需要將任務加入event隊列可以使用new Future
//向event隊列中添加一個任務
new Future(() {
//任務具體代碼
});
你也可以使用then或者whenComplete在Future結束后立刻執行某段代碼。如下面這段代碼在這個Future被執行后會立刻輸出42:
new Future(() => 21)
.then((v) => v*2)
.then((v) => print(v));
如果要在一段時間后添加一個任務,可以使用new Future.delayed():
// 一秒以后將任務添加至event隊列
new Future.delayed(const Duration(seconds:1), () {
//任務具體代碼
});
雖然上面這個例子中一秒后向event隊列添加一個任務,但是這個任務想要被執行的話必須滿足一下幾點:
- main方法執行完畢
- microtask隊列為空
- 該任務前的任務全部執行完畢
所以該任務真正被執行可能是大於1秒后。
關於Future的有趣事實:
- 被添加到then()中的方法會在Future執行后立馬執行(這方法沒有被加入任何隊列,只是被回調了)。
- 如果在then()調用之前Future就已經執行完畢了,那么會有一個任務被加入到microtask隊列中。這個任務執行的就是被傳入then的方法。
- Future()和Future.delayed()構造方法並不會被立刻完成,他們會向event隊列中添加一個任務。
- Future.value()構造方法會在一個microtask中完成。
- Future,sync()構造方法會立馬執行其參數方法,並在microtask中完成。
Microtask隊列: scheduleMicrotask()
dart:async定義了一個頂級方法scheduleMicrotask() ,你可以這樣使用:
scheduleMicrotask(() {
// ...code goes here...
});
如果有必要可以使用isolate或worker
如果你想要完成一些重量級的任務,為了保證你應用可響應,你應該將任務添加到isolate或者worker中。isolate可能會運行在不同的進程或線程中.這取決於Dart的具體實現。
那一般情況下你應該使用多少個isolate來完成你的工作呢?通常情況下可以根據你的cpu的個數來決定。
但你也可以使用超過cpu個數的isolate,前提是你的app能有一個好的架構。讓不同的isolate來分擔不同的代碼塊運行,但這前提是你能保證這些isolate之間沒有數據的共享。
測試一下你的理解程度
目前為止你已經掌握了調度任務的基本知識,下面來測試一下你的理解程度。
問題1
下面這段代碼的輸出是什么?
import 'dart:async';
main() {
print('main #1 of 2');
scheduleMicrotask(() => print('microtask #1 of 2'));
new Future.delayed(new Duration(seconds:1),
() => print('future #1 (delayed)'));
new Future(() => print('future #2 of 3'));
new Future(() => print('future #3 of 3'));
scheduleMicrotask(() => print('microtask #2 of 2'));
print('main #2 of 2');
}
別急着看答案,自己在紙上寫寫答案呢?
答案:
main #1 of 2
main #2 of 2
microtask #1 of 2
microtask #2 of 2
future #2 of 3
future #3 of 3
future #1 (delayed)
上面的答案是否就是你所期望的呢?這段代碼一共執行了三個分支:
- main()方法
- microtask隊列
- event隊列(先new Future后new Future.delayed)
main方法中的普通代碼都是同步執行的,所以肯定是main打印先全部打印出來,等main方法結束后會開始檢查microtask中是否有任務,若有則執行,執行完繼續檢查microtask,直到microtask列隊為空。所以接着打印的應該是microtask的打印。最后會去執行event隊列。由於有一個使用的delay方法,所以它的打印應該是在最后的。
問題2
下面這個問題相對有些復雜:
import 'dart:async';
main() {
print('main #1 of 2');
scheduleMicrotask(() => print('microtask #1 of 3'));
new Future.delayed(new Duration(seconds:1),
() => print('future #1 (delayed)'));
new Future(() => print('future #2 of 4'))
.then((_) => print('future #2a'))
.then((_) {
print('future #2b');
scheduleMicrotask(() => print('microtask #0 (from future #2b)'));
})
.then((_) => print('future #2c'));
scheduleMicrotask(() => print('microtask #2 of 3'));
new Future(() => print('future #3 of 4'))
.then((_) => new Future(
() => print('future #3a (a new future)')))
.then((_) => print('future #3b'));
new Future(() => print('future #4 of 4'));
scheduleMicrotask(() => print('microtask #3 of 3'));
print('main #2 of 2');
}
答案:
main #1 of 2
main #2 of 2
microtask #1 of 3
microtask #2 of 3
microtask #3 of 3
future #2 of 4
future #2a
future #2b
future #2c
microtask #0 (from future #2b)
future #3 of 4
future #4 of 4
future #3a (a new future)
future #3b
future #1 (delayed)
總結
以下有幾點關於dart的事件循環機制需要牢記於心:
- Dart事件循環執行兩個隊列里的事件:event隊列和microtask隊列。
- event隊列的事件來自dart(future,timer,isolate message等)和系統(用戶輸入,I/O等)。
- 目前為止,microtask隊列的事件只來自dart。
- 事件循環會優先清空microtask隊列,然后才會去處理event隊列。
- 當兩個隊列都清空后,dart就會退出。
- main方法,來自event隊列和microtask隊列的所有事件都運行在Dart的main isolate中。
當你要安排一個任務時,請遵守以下規則:
- 如果可以,盡量將任務放入event隊列中。
- 使用Future的then方法或whenComplete方法來指定任務順序。
- 為了保持你app的可響應性,盡量不要將大計算量的任務放入這兩個隊列。
- 大計算量的任務放入額外的isolate中。