詳解C#中的Async和Await用法
這篇文章由Filip Ekberg為DNC雜志編寫。
自跟隨著.NET 4.5 及Visual Studio 2012的C# 5.0起,我們能夠使用涉及到async和await關鍵字的新的異步模式。有很多不同觀點認為,比起以前我們看到的,它的可讀性和可用性是否更為突出。我們將通過一個例子來看下它跟現(xiàn)在的怎么不同。
線性代碼vs非線性代碼
大部分的軟件工程師都習慣用一種線性的方式去編程,至少這是他們開始職業(yè)生涯時就被這樣教導。當一個程序使用線性方式去編寫,這意味著它的源代碼讀起來有的像Figure 1展示的。這就是假設有一個適當?shù)挠唵蜗到y(tǒng)會幫助我們從某些地方去取一批訂單。
即使文章從左或從由開始,人們還是習慣于從上到下地閱讀。如果我們有某些東西影響到了這個內(nèi)容的順序,我們將會感到困惑同時在這上面比實際需要的事情上花費更多努力?;谑录某绦蛲ǔ碛羞@些非線性的結構。
基于事件系統(tǒng)的流程是這樣的,它在某處發(fā)起一個調(diào)用同時期待結果通過一個觸發(fā)的時間傳遞,F(xiàn)igure 2 展示的很形象的表達了這點。初看這兩個序列似乎不是很大區(qū)別,但如果我們假設GetAllOrders返回空,我們檢索訂單列表就沒那么直接了當了。
不看實際的代碼,我們認為線性方法處理起來更加舒服,同時它更少的有出錯的傾向。在這種情況下,錯誤可能不是實際的運行時錯誤或者編譯錯誤,但是在使用上的錯誤;由于缺乏明朗。
基于事件的方法有一個很大的優(yōu)勢;它讓我們使用基于事件的異步模式更為一致。
在你看到一個方法的時候,你會想去弄明白這方法的目的。這意味著如果你有一個叫ReloadOrdersAndRefreshUI的方法,你想去弄明白這些訂單從哪里載入,怎樣把它加到UI,當這方法結束的時候會發(fā)生什么。在基于事件的方法里,這很難如愿以償。
另外得益于這的是,只要在我們出發(fā)LoadOrdersCompleted事件時,我們能夠在GetAllOrders里寫異步代碼,返回到調(diào)用線程去。
介紹一個新的模式
讓 我們假設我們在自己的系統(tǒng)上工作,系統(tǒng)使用上面提到過的OrderHandler以及實際實現(xiàn)是使用一個線性方法。為了模擬一小部分的真是訂單系統(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; } }
因為我們在例子里不使用真是的數(shù)據(jù)源,我們需要讓它有那么一點更為有趣的。由于這是關于異步編程的,我們想要在一個異步的方式中請求一些東西。為了模擬這個,我們簡單的加入:
System.Threading.ManualResetEvent(false).WaitOne(2000) in GetAllOrders: public IEnumerable<Order> GetAllOrders() { System.Threading.ManualResetEvent(false).WaitOne(2000); return _orders; }
這里我們不用Thread.Sleep的原因是這段代碼將會加入到Windows8商店應用程序。這里的目的是在這里我們將會為我們的加載訂單列表的Windows8商店應用程序放置一個可以按的按鈕。然后,我們可以比較下用戶體驗和在之前加入的異步代碼。
如果你已經(jīng)創(chuàng)建了一個空的Windows商店應用程序項目,你可以加入如下的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; }
這會帶給我們一個挺好看的應用程序,當我們在Visual Studio 2012的模擬器上運行的時候看起來就像這樣:
看下底部的應用程序工具欄, 通過按這個在右手邊的菜單的圖標進入基本的觸摸模式,然后從下往上刷。
現(xiàn)在當你按下加載訂單按鈕的時候,你會注意到你看不到進度條同時按鈕保持在被按下狀態(tài)2秒。這是由于我們把應用程序鎖定了。
以前我們可以通過在一個BackgroundWorker里封裝代碼來解決問題。當完成的時候,它會在我們?yōu)楦淖僓I而已調(diào)用的委托中出發(fā)一個事件。這是一種非線性的方法,但往往會把代碼的可讀性搞得糟糕。在一個非WinRT的訂單應用程序,使用BackgroundWorker應該看起來像這樣:
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由于基于事件的異步性而被認識,這種模式叫做基于事件異步模式(EAP)。這往往會使代碼比以前更亂,同時,由于它使用非線性方式編寫,我們的腦袋要花一段事件才能對它有一定的概念。
但在WinRT中沒有BackgroundWorker,所以我們必須適應新的線性方法,這也是一個好的事情!
我們對此的解決方法是適應.NET4.5引入的新的模式,async 與 await。當我們使用async 和 await,就必須同時使用任務并行庫(TPL)。原則是每當一個方法需要異步執(zhí)行,我們就給它這個標記。這意味著該方法將帶著一些我們等待的東西返回,一個繼續(xù)點。繼續(xù)點段所在位置的標記,是由‘a(chǎn)waitable'的標記指明的,此后我們請求等待任務完成。
基于原始代碼,沒有BackgroundWorker的話我們只能對click處理代碼做一些小的改變,以便它能應用于異步的方式。首先我們需要標記該方法為異步的,這簡單到只需將關鍵字加到方法簽名:
private async void LoadOrders_Click(object sender, RoutedEventArgs e)
同時使用async和void時需要很小心,標記一個異步的方法返回值為void的唯一原因,就是因為事件處理代碼。當方法不是事件處理者,且返回類型為空時,絕不要標記其為異步的!異步與等待總是同時使用的,如果一個方法標記為異步的但其內(nèi)部卻沒有什么可等待的,它將只會以同步方式執(zhí)行。
因此下一個我們要做的事情事實上就是保證有一些我們能等待的事情,在我們的例子中就是調(diào)用GetAllOrders。由于這是最耗費時間的部分,我們希望它可以在一個獨立的task中執(zhí)行。我們只需將這個方法打包于一個期待返回IEnumerable<Order>的task,就像這樣:
Task<IEnumerable<Order>>.Factory.StartNew(() => { return orderHandler.GetAllOrders(); });
上面就是我們要等待的部分,我們來看看開始我們有的并對比一下現(xiàn)在我們有的:
// Before var orders = orderHandler.GetAllOrders(); // After var orders = await Task<IEnumerable<Order>>.Factory.StartNew(() => { return orderHandler.GetAllOrders(); });
當我們在一個task前增加了等待,訂單變量的類型就是task期待返回的類型;在這個例子中是IEnumerable<Order>。這意味著我們要使這個方法異步,需要唯一做的就是標記它是異步的,并且將對執(zhí)行時間長的方法的調(diào)用封裝于一個task之內(nèi)。
內(nèi)部發(fā)生的事情就是我們將用一個狀態(tài)機保存task執(zhí)行結束的印記。等待代碼段的所有代碼將被放入一個繼續(xù)點代碼段。如果你對TPL和task的繼續(xù)點熟悉,這就與之類似,除了我們到達繼續(xù)點便回到了調(diào)用線程之外!這是一個重要的區(qū)別,因為那意味著我們可以使我們的方法像這樣,而不需要任何分派器的調(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時用到的分派器?,F(xiàn)在我們可以執(zhí)行這個應用并且裝載訂單而不鎖定UI,并且然后會很漂亮的獲得許多訂單列表的顯示。
新方法帶來的好處事顯而易見的,它使得代碼更線性、更具可讀性。 當然,即使是最好的模式,也能寫出難看的代碼。 異步和待機確實能夠使代碼更可讀、更易于維護。
結論
Async & Await 使得創(chuàng)建一個具有可讀性與可維護性的異步解決方案變得很容易。在本文發(fā)布前,我們不得不求助于可能引起困惑的基于事件的方法。由于我們已處于幾乎所有電腦,甚至手機都有至少兩個內(nèi)核的時代,我們將會看到更多的并行的異步的代碼。因為這些使得async & await 很容易,所以在開發(fā)階段引入這個問題已沒有必要。我們能避免由于沒有調(diào)度程序或調(diào)度功能而采用任務或基于事件的異步性所引起的跨線程的問題。隨著這個新的模式,我們可以不再陷入聚焦于創(chuàng)建可響應可維護的解決方案的思考。
當然,這并非萬能的??傆羞@個方法也會導致混亂的情形。但只要在適當?shù)牡胤绞褂盟?,將有益于應用的生命周期?
相關文章
C#開發(fā)微信門戶及應用(4) 關注用戶列表及詳細信息管理
這篇文章主要為大家詳細介紹了C#開發(fā)微信門戶及應用第四篇,關注用戶列表及詳細信息管理,具有一定的參考價值,感興趣的小伙伴們可以參考一下2017-06-06