1. 程式人生 > >C#沉澱-非同步程式設計 三

C#沉澱-非同步程式設計 三

GUI程式在設計上要求所有的顯示變化都必須在主GUI執行緒中完成,Windows程式是通過訊息來實現這一點的,訊息被放入由訊息泵管理的訊息佇列中。

訊息泵從列隊中取出一條訊息,並呼叫它的處理程式程式碼。當程式程式碼完成時,訊息泵獲取下一條訊息並迴圈這個過程。

由於這個架構,處理程式程式碼就必須矮小精悍,這樣才不至於扶起並阻礙其他GUI行為處理。如果某個訊息的處理程式程式碼耗時過長,訊息佇列中的訊息會產生積壓。程式將失去響應,因為在那個長時間執行的處理程式完成之前,無法處理任何訊息。

示例:下面建立一個WPF程式,在主窗體中新增一個Label與一個Button,具體工作在後端程式碼的註釋中

<
Window x:Class="WpfForAsync.MainWindow" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:
local="clr-namespace:WpfForAsync" mc:Ignorable="d" Title="MainWindow" Height="151.4" Width="323.2"> <StackPanel> <Label Name="lblStatus" Margin="10,5,10,0" >Not Doing Anything</Label> <Button Name="btnDoStuff" Content="Do Stuff" HorizontalAlignment=
"Left" Margin="10,5" Padding="5,2" Click="btnDoStuff_Click"/> </StackPanel> </Window>

後端程式碼:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;

namespace WpfForAsync
{
    /// <summary>
    /// MainWindow.xaml 的互動邏輯
    /// </summary>
    public partial class MainWindow : Window
    {
        public MainWindow()
        {
            InitializeComponent();
        }

        private void btnDoStuff_Click(object sender, RoutedEventArgs e)
        {
            //首先讓按鈕禁用
            btnDoStuff.IsEnabled = false;
            //第二步將lable的文字改動
            lblStatus.Content = "Doing Stuff";
            //阻塞4秒鐘
            Thread.Sleep(1000 * 4);
            //將lable的內容改為原來的內容
            lblStatus.Content = "Not Doing Anything";
            //將按鈕設定為可用
            btnDoStuff.IsEnabled = true;
        }
    }
}

事實上,當點選按鈕的時候,窗體沒有任何變化,Label也不會發生內容更改的情況,而且,主窗體在被阻塞的4秒內也無被凍結了!!!

來看程式執行的順序:

  1. btnDoStuff_Click將[禁用按鈕]訊息壓入佇列
  2. btnDoStuff_Click將[改變文字]訊息壓入佇列
  3. btnDoStuff_Click將被阻塞4秒
  4. btnDoStuff_Click將[改變文字]訊息壓入佇列
  5. btnDoStuff_Click將[啟用按鈕]訊息壓入佇列
  6. btnDoStuff_Click將退出
  7. 程式執行[禁用按鈕]
  8. 程式執行[改變文字]
  9. 程式執行[改變文字]
  10. 程式執行[啟用按鈕]

由於7~10發生的太快,我們根本看不到發生了什麼!

示例:使用非同步來解決上述問題

後端程式碼:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;

namespace WpfForAsync
{
    /// <summary>
    /// MainWindow.xaml 的互動邏輯
    /// </summary>
    public partial class MainWindow : Window
    {
        public MainWindow()
        {
            InitializeComponent();
        }

        private async void btnDoStuff_Click(object sender, RoutedEventArgs e)
        {
            //首先讓按鈕禁用
            btnDoStuff.IsEnabled = false;
            //第二步將lable的文字改動
            lblStatus.Content = "Doing Stuff";

            await Task.Delay(1000 * 4);

            //將lable的內容改為原來的內容
            lblStatus.Content = "Not Doing Anything";
            //將按鈕設定為可用
            btnDoStuff.IsEnabled = true;
        }
    }
}

將btnDoStuff_Click方法改為一個非同步方法,非同步方法內部使用await Task.Delay(1000 * 4);,即將遇到await的時候,程式會返回到呼叫方法,處理程式將從處理器上摘下,禁用按鈕、改變文字將被處理;當4秒之後,處理程式將自己再壓入佇列,再將之後的改變文字、啟用按鈕壓入列隊,當處理程式退出後,這兩個子訊息也將被執行,但在休息的4秒期間,是可以手動窗體的

Task.Yield

Task.Yield方法可以建立一個立即返回的awaitable。等等一個Yield可以讓非同步方法在執行後續部分的同時返回到呼叫方法。可以將其理解成離開當前的訊息佇列,回到佇列末尾,讓處理器有時間處理其他任務。

示例:

using System;
using System.Net;
using System.Diagnostics;
using System.Threading.Tasks;
using System.Collections.Generic;
using System.Threading;

namespace CodeForAsync
{
    static class DoStuff
    {
        public static async Task<int> FindSeriesSum(int x)
        {
            int sum = 0;
            for (int i = 1; i < x; i++)
            {
                sum += i;
                Console.WriteLine("Yield"+i);
                if (i % 10 == 0)
                    await Task.Yield();
            }

            return sum;
        }
    }

    class Program
    {
        private static void CountBig(int p)
        {
            for (int i = 0; i < p; i++)
                Console.WriteLine("CountBig"+i); 
        }

        static void Main(string[] args)
        {
            Task<int> value = DoStuff.FindSeriesSum(1000);
            CountBig(1000);
            CountBig(1000);
            CountBig(1000);
            CountBig(1000);
            Console.WriteLine("Sum:{0}",value.Result);
            Console.ReadKey();
        }
    }
}

當程式遇到await Task.Yield();時,會將當前的執行權轉讓出來,之後的CountBig(1000);便可以提前得到執行

使用非同步Lambda表示式

語法示例:

btn.Click += async (sender, e) => {//處理工作};

完整程式碼示例:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;

namespace WpfForAsync
{
    /// <summary>
    /// MainWindow.xaml 的互動邏輯
    /// </summary>
    public partial class MainWindow : Window
    {
        public MainWindow()
        {
            InitializeComponent();

            this.btnDoStuff.Click += async (sender, e) =>
            {
                btnDoStuff.IsEnabled = false;
                lblStatus.Content = "Hello";
                await Task.Delay(4000);
                btnDoStuff.IsEnabled = true;
                lblStatus.Content = "World";
            };
        }

    }
}

一個完整的GUI示例,請自行嘗試

<Window x:Class="WpfForAsync.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:local="clr-namespace:WpfForAsync"
        mc:Ignorable="d"
        Title="MainWindow" Height="151.4" Width="323.2" Loaded="Window_Loaded">
    <StackPanel>
        <Button Name="btnProcess" Width="100" Click="btnProcess_Click">Process</Button>
        <Button Name="btnCancel" Width="100" Click="progressBar_Click" >Cancel</Button>
        <ProgressBar Name="progressBar" Height="20" Width="200" Margin="10" HorizontalAlignment="Right"></ProgressBar>
    </StackPanel>
</Window>

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;
using System.Threading.Tasks;

namespace WpfForAsync
{
    /// <summary>
    /// MainWindow.xaml 的互動邏輯
    /// </summary>
    public partial class MainWindow : Window
    {
        CancellationTokenSource _cancellationTokenSource;
        CancellationToken _cancellationToken;

        public MainWindow()
        {
            InitializeComponent();
        }

        private void Window_Loaded(object sender, RoutedEventArgs e)
        {

        }

        private async void btnProcess_Click(object sender, RoutedEventArgs e)
        {
            btnProcess.IsEnabled = false;

            _cancellationTokenSource = new CancellationTokenSource();
            _cancellationToken = _cancellationTokenSource.Token;

            int completedPercent = 0;
            for (int i = 0; i < 10; i++)
            {
                if (_cancellationToken.IsCancellationRequested)
                    break;
                try
                {
                    await Task.Delay(500,_cancellationToken);
                    completedPercent = (i+1) * 10;
                }
                catch (TaskCanceledException ex)
                {
                    completedPercent = i * 10;
                }

                progressBar.Value = completedPercent;
            }
            string message = _cancellationToken.IsCancellationRequested ? string.Format("Process was cancelled at {0}%",completedPercent) : "Process completed normally";
            MessageBox.Show(message,"Completion Status");

            progressBar.Value = 0;
            btnProcess.IsEnabled = true;
            btnCancel.IsEnabled = true;
        }

        private void progressBar_Click(object sender, RoutedEventArgs e)
        {
            if (!btnProcess.IsEnabled)
            {
                btnCancel.IsEnabled = false;
                _cancellationTokenSource.Cancel();
            }
        }
    }
}