詳解C#中的Async和Await用法
這篇文章由Filip Ekberg為DNC雜志編寫。
自跟隨著.NET 4.5 及Visual Studio 2012的C# 5.0起,我們能夠使用涉及到async和await關(guān)鍵字的新的異步模式。有很多不同觀點(diǎn)認(rèn)為,比起以前我們看到的,它的可讀性和可用性是否更為突出。我們將通過一個(gè)例子來看下它跟現(xiàn)在的怎么不同。
線性代碼vs非線性代碼
大部分的軟件工程師都習(xí)慣用一種線性的方式去編程,至少這是他們開始職業(yè)生涯時(shí)就被這樣教導(dǎo)。當(dāng)一個(gè)程序使用線性方式去編寫,這意味著它的源代碼讀起來有的像Figure 1展示的。這就是假設(shè)有一個(gè)適當(dāng)?shù)挠唵蜗到y(tǒng)會(huì)幫助我們從某些地方去取一批訂單。
即使文章從左或從由開始,人們還是習(xí)慣于從上到下地閱讀。如果我們有某些東西影響到了這個(gè)內(nèi)容的順序,我們將會(huì)感到困惑同時(shí)在這上面比實(shí)際需要的事情上花費(fèi)更多努力?;谑录某绦蛲ǔ碛羞@些非線性的結(jié)構(gòu)。
基于事件系統(tǒng)的流程是這樣的,它在某處發(fā)起一個(gè)調(diào)用同時(shí)期待結(jié)果通過一個(gè)觸發(fā)的時(shí)間傳遞,F(xiàn)igure 2 展示的很形象的表達(dá)了這點(diǎn)。初看這兩個(gè)序列似乎不是很大區(qū)別,但如果我們假設(shè)GetAllOrders返回空,我們檢索訂單列表就沒那么直接了當(dāng)了。
不看實(shí)際的代碼,我們認(rèn)為線性方法處理起來更加舒服,同時(shí)它更少的有出錯(cuò)的傾向。在這種情況下,錯(cuò)誤可能不是實(shí)際的運(yùn)行時(shí)錯(cuò)誤或者編譯錯(cuò)誤,但是在使用上的錯(cuò)誤;由于缺乏明朗。
基于事件的方法有一個(gè)很大的優(yōu)勢(shì);它讓我們使用基于事件的異步模式更為一致。
在你看到一個(gè)方法的時(shí)候,你會(huì)想去弄明白這方法的目的。這意味著如果你有一個(gè)叫ReloadOrdersAndRefreshUI的方法,你想去弄明白這些訂單從哪里載入,怎樣把它加到UI,當(dāng)這方法結(jié)束的時(shí)候會(huì)發(fā)生什么。在基于事件的方法里,這很難如愿以償。
另外得益于這的是,只要在我們出發(fā)LoadOrdersCompleted事件時(shí),我們能夠在GetAllOrders里寫異步代碼,返回到調(diào)用線程去。
介紹一個(gè)新的模式
讓 我們假設(shè)我們?cè)谧约旱南到y(tǒng)上工作,系統(tǒng)使用上面提到過的OrderHandler以及實(shí)際實(shí)現(xiàn)是使用一個(gè)線性方法。為了模擬一小部分的真是訂單系統(tǒng),OrderHandler和Order如下:
class Order { public string OrderNumber { get; set; } public decimal OrderTotal { get; set; } public string Reference { get; set; } } class OrderHandler { private readonly IEnumerable<Order> _orders; public OrderHandler() { _orders = new[] { new Order {OrderNumber = "F1", OrderTotal = 100, Reference = "Filip"}, new Order {OrderNumber = "F1", OrderTotal = 100, Reference = "Filip"} }; } public IEnumerable<Order> GetAllOrders() { return _orders; } }
因?yàn)槲覀冊(cè)诶永锊皇褂谜媸堑臄?shù)據(jù)源,我們需要讓它有那么一點(diǎn)更為有趣的。由于這是關(guān)于異步編程的,我們想要在一個(gè)異步的方式中請(qǐng)求一些東西。為了模擬這個(gè),我們簡單的加入:
System.Threading.ManualResetEvent(false).WaitOne(2000) in GetAllOrders: public IEnumerable<Order> GetAllOrders() { System.Threading.ManualResetEvent(false).WaitOne(2000); return _orders; }
這里我們不用Thread.Sleep的原因是這段代碼將會(huì)加入到Windows8商店應(yīng)用程序。這里的目的是在這里我們將會(huì)為我們的加載訂單列表的Windows8商店應(yīng)用程序放置一個(gè)可以按的按鈕。然后,我們可以比較下用戶體驗(yàn)和在之前加入的異步代碼。
如果你已經(jīng)創(chuàng)建了一個(gè)空的Windows商店應(yīng)用程序項(xiàng)目,你可以加入如下的XAML到你的MainPage.xml:
<Grid Background="{StaticResource ApplicationPageBackgroundThemeBrush}"> <Grid.RowDefinitions> <RowDefinition Height="140"/> <RowDefinition Height="*"/> </Grid.RowDefinitions> <TextBlock x:Name="pageTitle" Margin="120,0,0,0" Text="Order System" Style="{StaticResource PageHeaderTextStyle}" Grid.Column="1" IsHitTestVisible="false"/> <StackPanel Grid.Row="1" Margin="120,50,0,0"> <TextBlock x:Name="Information" /> <ProgressBar x:Name="OrderLoadingProgress" HorizontalAlignment="Left" Foreground="White" Visibility="Collapsed" IsIndeterminate="True" Width="100"> <ProgressBar.RenderTransform> <CompositeTransform ScaleX="5" ScaleY="5" /> </ProgressBar.RenderTransform> </ProgressBar> <ListView x:Name="Orders" DisplayMemberPath="OrderNumber" /> </StackPanel> <AppBar VerticalAlignment="Bottom" Grid.Row="1"> <Button Content="Load orders" x:Name="LoadOrders" Click="LoadOrders_Click" /> </AppBar> </Grid>
在我們的程序能跑之前,我們還需要在代碼文件里加入一些東西:
public MainPage() { this.InitializeComponent(); Information.Text = "No orders have been loaded yet."; } private void LoadOrders_Click(object sender, RoutedEventArgs e) { OrderLoadingProgress.Visibility = Visibility.Visible; var orderHandler = new OrderHandler(); var orders = orderHandler.GetAllOrders(); OrderLoadingProgress.Visibility = Visibility.Collapsed; }
這會(huì)帶給我們一個(gè)挺好看的應(yīng)用程序,當(dāng)我們?cè)赩isual Studio 2012的模擬器上運(yùn)行的時(shí)候看起來就像這樣:
看下底部的應(yīng)用程序工具欄, 通過按這個(gè)在右手邊的菜單的圖標(biāo)進(jìn)入基本的觸摸模式,然后從下往上刷。
現(xiàn)在當(dāng)你按下加載訂單按鈕的時(shí)候,你會(huì)注意到你看不到進(jìn)度條同時(shí)按鈕保持在被按下狀態(tài)2秒。這是由于我們把應(yīng)用程序鎖定了。
以前我們可以通過在一個(gè)BackgroundWorker里封裝代碼來解決問題。當(dāng)完成的時(shí)候,它會(huì)在我們?yōu)楦淖僓I而已調(diào)用的委托中出發(fā)一個(gè)事件。這是一種非線性的方法,但往往會(huì)把代碼的可讀性搞得糟糕。在一個(gè)非WinRT的訂單應(yīng)用程序,使用BackgroundWorker應(yīng)該看起來像這樣:
public sealed partial class MainPage : Page { private BackgroundWorker _worker = new BackgroundWorker(); public MainPage() { InitializeComponent(); _worker.RunWorkerCompleted += WorkerRunWorkerCompleted; _worker.DoWork += WorkerDoWork; } void WorkerDoWork(object sender, DoWorkEventArgs e) { var orderHandler = new OrderHandler(); var orders = orderHandler.GetAllOrders(); } private void LoadOrders_Click(object sender, RoutedEventArgs e) { OrderLoadingProgress.Visibility = Visibility.Visible; _worker.RunWorkerAsync(); } void WorkerRunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e) { Dispatcher.BeginInvoke(new Action(() => { // Update the UI OrderLoadingProgress.Visibility = Visibility.Collapsed; })); } }
BackgroundWorker由于基于事件的異步性而被認(rèn)識(shí),這種模式叫做基于事件異步模式(EAP)。這往往會(huì)使代碼比以前更亂,同時(shí),由于它使用非線性方式編寫,我們的腦袋要花一段事件才能對(duì)它有一定的概念。
但在WinRT中沒有BackgroundWorker,所以我們必須適應(yīng)新的線性方法,這也是一個(gè)好的事情!
我們對(duì)此的解決方法是適應(yīng).NET4.5引入的新的模式,async 與 await。當(dāng)我們使用async 和 await,就必須同時(shí)使用任務(wù)并行庫(TPL)。原則是每當(dāng)一個(gè)方法需要異步執(zhí)行,我們就給它這個(gè)標(biāo)記。這意味著該方法將帶著一些我們等待的東西返回,一個(gè)繼續(xù)點(diǎn)。繼續(xù)點(diǎn)段所在位置的標(biāo)記,是由‘a(chǎn)waitable'的標(biāo)記指明的,此后我們請(qǐng)求等待任務(wù)完成。
基于原始代碼,沒有BackgroundWorker的話我們只能對(duì)click處理代碼做一些小的改變,以便它能應(yīng)用于異步的方式。首先我們需要標(biāo)記該方法為異步的,這簡單到只需將關(guān)鍵字加到方法簽名:
private async void LoadOrders_Click(object sender, RoutedEventArgs e)
同時(shí)使用async和void時(shí)需要很小心,標(biāo)記一個(gè)異步的方法返回值為void的唯一原因,就是因?yàn)槭录幚泶a。當(dāng)方法不是事件處理者,且返回類型為空時(shí),絕不要標(biāo)記其為異步的!異步與等待總是同時(shí)使用的,如果一個(gè)方法標(biāo)記為異步的但其內(nèi)部卻沒有什么可等待的,它將只會(huì)以同步方式執(zhí)行。
因此下一個(gè)我們要做的事情事實(shí)上就是保證有一些我們能等待的事情,在我們的例子中就是調(diào)用GetAllOrders。由于這是最耗費(fèi)時(shí)間的部分,我們希望它可以在一個(gè)獨(dú)立的task中執(zhí)行。我們只需將這個(gè)方法打包于一個(gè)期待返回IEnumerable<Order>的task,就像這樣:
Task<IEnumerable<Order>>.Factory.StartNew(() => { return orderHandler.GetAllOrders(); });
上面就是我們要等待的部分,我們來看看開始我們有的并對(duì)比一下現(xiàn)在我們有的:
// Before var orders = orderHandler.GetAllOrders(); // After var orders = await Task<IEnumerable<Order>>.Factory.StartNew(() => { return orderHandler.GetAllOrders(); });
當(dāng)我們?cè)谝粋€(gè)task前增加了等待,訂單變量的類型就是task期待返回的類型;在這個(gè)例子中是IEnumerable<Order>。這意味著我們要使這個(gè)方法異步,需要唯一做的就是標(biāo)記它是異步的,并且將對(duì)執(zhí)行時(shí)間長的方法的調(diào)用封裝于一個(gè)task之內(nèi)。
內(nèi)部發(fā)生的事情就是我們將用一個(gè)狀態(tài)機(jī)保存task執(zhí)行結(jié)束的印記。等待代碼段的所有代碼將被放入一個(gè)繼續(xù)點(diǎn)代碼段。如果你對(duì)TPL和task的繼續(xù)點(diǎn)熟悉,這就與之類似,除了我們到達(dá)繼續(xù)點(diǎn)便回到了調(diào)用線程之外!這是一個(gè)重要的區(qū)別,因?yàn)槟且馕吨覀兛梢允刮覀兊姆椒ㄏ襁@樣,而不需要任何分派器的調(diào)用:
private async void LoadOrders_Click(object sender, RoutedEventArgs e) { OrderLoadingProgress.Visibility = Visibility.Visible; var orderHandler = new OrderHandler(); var orderTask = Task<IEnumerable<Order>>.Factory.StartNew(() => { return orderHandler.GetAllOrders(); }); var orders = await orderTask; Orders.Items.Clear(); foreach (var order in orders) Orders.Items.Add(order); OrderLoadingProgress.Visibility = Visibility.Collapsed; }
正如你看到的,我們只需在等待代碼段之后改變UI上的東西,而不需要使用我們前面在用EAP或TPL時(shí)用到的分派器?,F(xiàn)在我們可以執(zhí)行這個(gè)應(yīng)用并且裝載訂單而不鎖定UI,并且然后會(huì)很漂亮的獲得許多訂單列表的顯示。
新方法帶來的好處事顯而易見的,它使得代碼更線性、更具可讀性。 當(dāng)然,即使是最好的模式,也能寫出難看的代碼。 異步和待機(jī)確實(shí)能夠使代碼更可讀、更易于維護(hù)。
結(jié)論
Async & Await 使得創(chuàng)建一個(gè)具有可讀性與可維護(hù)性的異步解決方案變得很容易。在本文發(fā)布前,我們不得不求助于可能引起困惑的基于事件的方法。由于我們已處于幾乎所有電腦,甚至手機(jī)都有至少兩個(gè)內(nèi)核的時(shí)代,我們將會(huì)看到更多的并行的異步的代碼。因?yàn)檫@些使得async & await 很容易,所以在開發(fā)階段引入這個(gè)問題已沒有必要。我們能避免由于沒有調(diào)度程序或調(diào)度功能而采用任務(wù)或基于事件的異步性所引起的跨線程的問題。隨著這個(gè)新的模式,我們可以不再陷入聚焦于創(chuàng)建可響應(yīng)可維護(hù)的解決方案的思考。
當(dāng)然,這并非萬能的??傆羞@個(gè)方法也會(huì)導(dǎo)致混亂的情形。但只要在適當(dāng)?shù)牡胤绞褂盟?,將有益于?yīng)用的生命周期。
相關(guān)文章
C#實(shí)現(xiàn)XOR密碼(異或密碼)的示例代碼
XOR密碼(異或密碼)是一種簡單的加密算法,它使用異或(XOR)操作來對(duì)明文和密鑰進(jìn)行加密和解密,本文為大家介紹了C#實(shí)現(xiàn)XOR密碼的相關(guān)知識(shí),希望對(duì)大家有所幫助2024-01-01C# Winform實(shí)現(xiàn)表格復(fù)制粘貼效果
這篇文章主要為大家學(xué)習(xí)介紹了如何通過C# Winform實(shí)現(xiàn)表格復(fù)制粘貼效果,文中的示例代碼講解詳細(xì),具有一定的參考價(jià)值,需要的可以了解一下2023-07-07C#開發(fā)微信門戶及應(yīng)用(4) 關(guān)注用戶列表及詳細(xì)信息管理
這篇文章主要為大家詳細(xì)介紹了C#開發(fā)微信門戶及應(yīng)用第四篇,關(guān)注用戶列表及詳細(xì)信息管理,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2017-06-06c# Form中的鍵盤響應(yīng)具體實(shí)現(xiàn)思路
在全屏Form中加上鍵盤ESC的響應(yīng),實(shí)現(xiàn)的效果就是:全屏中press鍵盤上的Escape鍵,程序結(jié)束,具體實(shí)現(xiàn)步驟如下,感興趣的朋友可以參考下哈2013-06-06