C# 5.0 的 Async 和 Await (翻譯)

這篇文章由Filip Ekberg爲DNC雜誌編寫。php

自跟隨着.NET 4.5 及Visual Studio 2012的C# 5.0起,咱們可以使用涉及到async和await關鍵字的新的異步模式。有不少不一樣觀點認爲,比起之前咱們看到的,它的可讀性和可用性是否更爲突出。咱們將經過一個例子來看下它跟如今的怎麼不一樣。編程

 

線性代碼vs非線性代碼

大部分的軟件工程師都習慣用一種線性的方式去編程,至少這是他們開始職業生涯時就被這樣教導。當一個程序使用線性方式去編寫,這意味着它的源代碼讀起來有的像Figure 1展現的。這就是假設有一個適當的訂單系統會幫助咱們從某些地方去取一批訂單。c#

linear-code

即便文章從左或從由開始,人們仍是習慣於從上到下地閱讀。若是咱們有某些東西影響到了這個內容的順序,咱們將會感到困惑同時在這上面比實際須要的事情上花費更多努力。基於事件的程序一般擁有這些非線性的結構。app

 

基於事件系統的流程是這樣的,它在某處發起一個調用同時期待結果經過一個觸發的時間傳遞,Figure 2 展現的很形象的表達了這點。初看這兩個序列彷佛不是很大區別,但若是咱們假設GetAllOrders返回空,咱們檢索訂單列表就沒那麼直接了當了。異步

不看實際的代碼,咱們認爲線性方法處理起來更加舒服,同時它更少的有出錯的傾向。在這種狀況下,錯誤可能不是實際的運行時錯誤或者編譯錯誤,可是在使用上的錯誤;因爲缺少明朗。async

 

 

基於事件的方法有一個很大的優點;它讓咱們使用基於事件的異步模式更爲一致。異步編程

event-based-approach

在你看到一個方法的時候,你會想去弄明白這方法的目的。這意味着若是你有一個叫ReloadOrdersAndRefreshUI的方法,你想去弄明白這些訂單從哪裏載入,怎樣把它加到UI,當這方法結束的時候會發生什麼。在基於事件的方法裏,這很難如願以償。工具

另外得益於這的是,只要在咱們出發LoadOrdersCompleted事件時,咱們可以在GetAllOrders裏寫異步代碼,返回到調用線程去。this

 

 

介紹一個新的模式

讓 咱們假設咱們在本身的系統上工做,系統使用上面提到過的OrderHandler以及實際實現是使用一個線性方法。爲了模擬一小部分的真是訂單系統,OrderHandler和Order以下:spa

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的模擬器上運行的時候看起來就像這樣:

order-system

看下底部的應用程序工具欄, 經過按這個在右手邊的菜單的圖標 clip_image002 進入基本的觸摸模式,而後從下往上刷。

 

 

如今當你按下加載訂單按鈕的時候,你會注意到你看不到進度條同時按鈕保持在被按下狀態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(); });

上面就是咱們要等待的部分,咱們來看看開始咱們有的並對比一下如今咱們有的:

// Before
var orders = orderHandler.GetAllOrders();

// After
var 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,而且而後會很漂亮的得到許多訂單列表的顯示。

 

 

order-system2

新方法帶來的好處事顯而易見的,它使得代碼更線性、更具可讀性。 固然,即便是最好的模式,也能寫出難看的代碼。 異步和待機確實可以使代碼更可讀、更易於維護。

 

結論

Async & Await 使得建立一個具備可讀性與可維護性的異步解決方案變得很容易。在本文發佈前,咱們不得不求助於可能引發困惑的基於事件的方法。因爲咱們已處於幾乎全部電腦,甚至手機都有至少兩個內核的時代,咱們將會看到更多的並行的異步的代碼。由於這些使得async & await 很容易,因此在開發階段引入這個問題已沒有必要。咱們能避免因爲沒有調度程序或調度功能而採用任務或基於事件的異步性所引發的跨線程的問題。隨着這個新的模式,咱們能夠再也不陷入聚焦於建立可響應可維護的解決方案的思考。

 

固然,這並不是萬能的。總有這個方法也會致使混亂的情形。但只要在適當的地方使用它,將有益於應用的生命週期。

相關文章
相關標籤/搜索