標籤:setevent oid 功能 UI 任務 方法簽名 結果 一個 程式碼片段
這篇文章由Filip Ekberg為DNC雜誌編寫。
自跟隨著.NET 4.5 及Visual Studio 2012的C# 5.0起,我們能夠使用涉及到async和await關鍵字的新的非同步模式。有很多不同觀點認為,比起以前我們看到的,它的可讀性和可用性是否更為突出。我們將通過一個例子來看下它跟現在的怎麼不同。
線性代碼vs非線性代碼
大部分的軟體工程師都習慣用一種線性方式去編程,至少這是他們開始職業生涯時就被這樣教導。當一個程式使用線性方式去編寫,這意味著它的原始碼讀起來有的像Figure 1展示的。這就是假設有一個適當的訂單系統會協助我們從某些地方去取一批訂單。
即使文章從左或從由開始,人們還是習慣於從上到下地閱讀。如果我們有某些東西影響到了這個內容的順序,我們將會感到困惑同時在這上面比實際需要的事情上花費更多努力。基於事件的程式通常擁有這些非線性結構。
基於事件系統的流程是這樣的,它在某處發起一個調用同時期待結果通過一個觸發的時間傳遞,Figure 2 展示的很形象的表達了這點。初看這兩個序列似乎不是很大區別,但如果我們假設GetAllOrders返回空,我們檢索訂單列表就沒那麼直接了當了。
不看實際的代碼,我們認為線性方法處理起來更加舒服,同時它更少的有出錯的傾向。在這種情況下,錯誤可能不是實際的執行階段錯誤或者編譯錯誤,但是在使用上的錯誤;由於缺乏明朗。
基於事件的方法有一個很大的優勢;它讓我們使用事件架構非同步模式更為一致。
在你看到一個方法的時候,你會想去弄明白這方法的目的。這意味著如果你有一個叫ReloadOrdersAndRefreshUI的方法,你想去弄明白這些訂單從哪裡載入,怎樣把它加到UI,當這方法結束的時候會發生什麼。在基於事件的方法裡,這很難如願以償。
另外得益於這的是,只要在我們出發LoadOrdersCompleted事件時,我們能夠在GetAllOrders裡寫非同步代碼,返回到調用線程去。
介紹一個新的模式
讓 我們假設我們在自己的系統上工作,系統使用上面提到過的OrderHandler以及實際實現是使用一個線性方法。為了類比一小部分的真是訂單系統,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; }}
因為我們在例子裡不使用真是的資料來源,我們需要讓它有那麼一點更為有趣的。由於這是關於非同步編程的,我們想要在一個非同步方式中請求一些東西。為了類比這個,我們簡單的加入:
System.Threading.ManualResetEvent(false).WaitOne(2000) in GetAllOrders:public IEnumerable<Order> GetAllOrders(){ System.Threading.ManualResetEvent(false).WaitOne(2000); return _orders;}
這裡我們不用Thread.Sleep的原因是這段代碼將會加入到Windows8商店應用程式。這裡的目的是在這裡我們將會為我們的載入訂單列表的Windows8商店應用程式放置一個可以按的按鈕。然後,我們可以比較下使用者體驗和在之前加入的非同步代碼。
如果你已經建立了一個空的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的模擬器上啟動並執行時候看起來就像這樣:
看下底部的應用程式工具列, 通過按這個在右手邊的菜單的表徵圖 進入基本的觸摸模式,然後從下往上刷。
現在當你按下載入訂單按鈕的時候,你會注意到你看不到進度條同時按鈕保持在被按下狀態2秒。這是由於我們把應用程式鎖定了。
以前我們可以通過在一個BackgroundWorker裡封裝代碼來解決問題。當完成的時候,它會在我們為改變UI而已調用的委託中出發一個事件。這是一種非線性方法,但往往會把代碼的可讀性搞得糟糕。在一個非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)。原則是每當一個方法需要非同步執行,我們就給它這個標記。這意味著該方法將帶著一些我們等待的東西返回,一個繼續點。繼續點段所在位置的標記,是由‘awaitable’的標記指明的,此後我們請求等待任務完成。
基於原始代碼,沒有BackgroundWorker的話我們只能對click處理代碼做一些小的改變,以便它能應用於非同步方式。首先我們需要標記該方法為非同步,這簡單到只需將關鍵字加到方法簽名:
private async void LoadOrders_Click(object sender, RoutedEventArgs e)
同時使用async和void時需要很小心,標記一個非同步方法傳回值為void的唯一原因,就是因為事件處理代碼。當方法不是事件處理者,且傳回型別為空白時,絕不要標記其為非同步!非同步與等待總是同時使用的,如果一個方法標記為非同步但其內部卻沒有什麼可等待的,它將只會以同步方式執行。
因此下一個我們要做的事情事實上就是保證有一些我們能等待的事情,在我們的例子中就是調用GetAllOrders。由於這是最耗費時間的部分,我們希望它可以在一個獨立的task中執行。我們只需將這個方法打包於一個期待返回IEnumerable<Order>的task,就像這樣:
Task<IEnumerable<Order>>.Factory.StartNew(() => { return orderHandler.GetAllOrders(); });
上面就是我們要等待的部分,我們來看看開始我們有的並對比一下現在我們有的:
// Beforevar orders = orderHandler.GetAllOrders();// Aftervar orders = await Task<IEnumerable<Order>>.Factory.StartNew(() => { return orderHandler.GetAllOrders(); });
當我們在一個task前增加了等待,訂單變數的類型就是task期待返回的類型;在這個例子中是IEnumerable<Order>。這意味著我們要使這個方法非同步,需要唯一做的就是標記它是非同步,並且將對執行時間長的方法的調用封裝於一個task之內。
內部發生的事情就是我們將用一個狀態機器儲存task執行結束的印記。等待程式碼片段的所有代碼將被放入一個繼續點程式碼片段。如果你對TPL和task的繼續點熟悉,這就與之類似,除了我們到達繼續點便回到了調用線程之外!這是一個重要的區別,因為那意味著我們可以使我們的方法像這樣,而不需要任何指派器的調用:
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時用到的指派器。現在我們可以執行這個應用並且裝載訂單而不鎖定UI,並且然後會很漂亮的獲得許多訂單列表的顯示。
新方法帶來的好處事顯而易見的,它使得代碼更線性、更具可讀性。 當然,即使是最好的模式,也能寫出難看的代碼。 非同步和待機確實能夠使代碼更可讀、更易於維護。
結論
Async & Await 使得建立一個具有可讀性與可維護性的非同步解決方案變得很容易。在本文發布前,我們不得不求助於可能引起困惑的基於事件的方法。由於我們已處於幾乎所有電腦,甚至手機都有至少兩個核心的時代,我們將會看到更多的並行的非同步代碼。因為這些使得async & await 很容易,所以在開發階段引入這個問題已沒有必要。我們能避免由於沒有發送器或調度功能而採用任務或基於事件的非同步性所引起的跨線程的問題。隨著這個新的模式,我們可以不再陷入聚焦於建立可響應可維護的解決方案的思考。
當然,這並非萬能的。總有這個方法也會導致混亂的情形。但只要在適當的地方使用它,將有益於應用的生命週期。
C# 5.0 的 Async 和 Await (翻譯)