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

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

BackgroundWorker類

async/await特性適合那些需要在後臺完成的不相關的小任務,有時候,需要另建一個執行緒,在後臺持續執行以完成某項工作,並不時地與主執行緒進行通訊,BackgroundWorker類就是為此而生。

BackgroundWorker類的主要成員

  • 屬性
    • WorkerReportsProgress 設定後臺任務是否可以把它的進度彙報給主執行緒
    • WorkerSupportsCancellation 是否支援從主執行緒取消
    • IsBusy 檢查後臺任務是否正在執行
    • CancelationPending 狀態值,布林型別
  • 方法
    • RunWorkerAsync() 獲取後臺執行緒並執行DoWork事件處理程式
    • CancelAsync() 把CancellationPending屬性設定為true。DoWork事件處理程式需要檢查這個屬性來決定是否應該停止處理
    • ReportProgress() DoWork工作的時候向主執行緒彙報進度
  • 事件
    • DoWork 在後臺執行緒開始的時候觸發
    • ProgressChanged 在後臺任務彙報狀態的時候觸發
    • RunWorkerCompleted 在後臺工作執行緒退出的時候觸發

使用示例:

using System.Threading;
using System.Windows;
using System.ComponentModel;

namespace WpfForAsync
{
/// <summary> /// MainWindow.xaml 的互動邏輯 /// </summary> public partial class MainWindow : Window { // 首先需要建立BackgroundWorker物件 BackgroundWorker bgWorker = new BackgroundWorker(); public MainWindow() { InitializeComponent(); // 設定BackgroundWorker屬性
// WorkerReportsProgress設定工作執行緒為主執行緒彙報進度 bgWorker.WorkerReportsProgress = true; // 設定支援從主執行緒取消工作執行緒 bgWorker.WorkerSupportsCancellation = true; // 連線BackgroundWorkder物件的處理程式 //DoWork是必須的 bgWorker.DoWork += BgWorker_DoWork; //ProgressChanged與RunWorkerCompleted這兩個是可選的 bgWorker.ProgressChanged += BgWorker_ProgressChanged; bgWorker.RunWorkerCompleted += BgWorker_RunWorkerCompleted; } private void BgWorker_RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e) { progressBar.Value = 0; if (e.Cancelled) { MessageBox.Show("Process was cancelled.", "Process Cancelled"); } else { MessageBox.Show("Process completed normally.", "Process Completed"); } } private void BgWorker_ProgressChanged(object sender, ProgressChangedEventArgs e) { progressBar.Value = e.ProgressPercentage; } /// <summary> /// 這裡是希望在後臺工作的程式碼 /// 它是在獨立的執行緒中執行的 /// </summary> /// <param name="sender"></param> /// <param name="e"></param> private void BgWorker_DoWork(object sender, DoWorkEventArgs e) { BackgroundWorker worker = sender as BackgroundWorker; for (int i = 0; i <= 10; i++) { if (worker.CancellationPending) { e.Cancel = true; break; } else { /// 後臺執行緒通過呼叫ReportProgress方法與主執行緒通訊。 /// 屆時將觸發ProgressChanged事件 /// 主執行緒可以處理附加到ProgressChanged事件上的處理程式 worker.ReportProgress(i * 10); Thread.Sleep(500); } } } private void btnProcess_Click(object sender, RoutedEventArgs e) { if (!bgWorker.IsBusy) { //主執行緒呼叫RunWorkerAsync的時候,會觸發DoWork事件 bgWorker.RunWorkerAsync(); } } private void progressBar_Click(object sender, RoutedEventArgs e) { // 通知後臺執行緒退出 // 此時會將後臺執行緒類的CancellationPending屬性設定為true // DoWork事件處理程式會定期檢查CancellationPending確定是否要退出 // 如果後臺執行緒退出,則會觸發RunWorkerCompleted事件 bgWorker.CancelAsync(); } } }

這是一個WPF程式,前端程式碼與上一節的一樣,後端程式碼使用後臺執行緒處理

這裡再來談一下每個事件處理程式形參中的引數類EventArgs的結構(所包含的屬性)

  • DoWorkEventArgs
    • Argument
    • Result
    • Cancel
  • ProgressChangedEventArgs
    • ProgressPercentage
    • UserState
  • RunWorkerCompletedEventArgs
    • Cancelled
    • Error
    • Result
    • UserState

並行迴圈

這裡介紹Parallel.For迴圈和Parallel.ForEach迴圈,它們都們於System.Threading.Tasks名稱空間下

這兩個迴圈可以使迭代之間彼此獨立,並且程式執行在多核處理器的機器上時,能將不同的迭代放在不同的處理器在上並行處理

Parallel.For方法有很多個過載,這裡介紹其中一個

public static ParallelLoopResult For(int fromInclusive, int toExclusive, Action<int> body);
  • fromInclusive引數是迭代系列的第一個整數
  • toExclusive引數是比迭代系列最後一個索引號大1的整數
  • body是接受單個輸入引數的委託,body的程式碼在每一次迭代中執行一次

示例:

using System;
using System.Threading.Tasks;  // 必須使用這個名稱空間

namespace CodeForAsync
{
    class Program
    {
        static void Main(string[] args)
        {
            // 從0開始迭代,直到14
            // 打印出索引與索引的平方
            Parallel.For(0, 15, i => 
                         Console.WriteLine("Thre Square of {0} is {1}", i, i * i));
            Console.ReadKey();
        }
    }
}

輸出:

Thre Square of 0 is 0
Thre Square of 2 is 4
Thre Square of 1 is 1
Thre Square of 4 is 16
Thre Square of 6 is 36
Thre Square of 7 is 49
Thre Square of 9 is 81
Thre Square of 10 is 100
Thre Square of 11 is 121
Thre Square of 12 is 144
Thre Square of 13 is 169
Thre Square of 8 is 64
Thre Square of 14 is 196
Thre Square of 3 is 9
Thre Square of 5 is 25

另一個示例:

using System;
using System.Threading.Tasks;

namespace CodeForAsync
{

    class Program
    {

        static void Main(string[] args)
        {
            const int maxValues = 50;
            int[] squares = new int[maxValues];

            Parallel.For(0, maxValues, i => squares[i] = i * i);

            for (int i = 0; i < squares.Length; i++)
            {
                Console.WriteLine(squares[i]);
            }

            Console.ReadKey();
        }
    }
}

本便將索引的平方新增到一個數組中,雖然是並行操作,但是數組裡的值卻是按順序按序的

Parallel.ForEach示例

Parallel.ForEach同樣有許多過載,這裡介紹一個簡單的

public static ParallelLoopResult ForEach<TSource>(IEnumerable<TSource> source, Action<TSource> body);
  • source是被操作的資料來源
  • body是一個無返回值的委託
using System;
using System.Threading.Tasks;

namespace CodeForAsync
{

    class Program
    {

        static void Main(string[] args)
        {
            string[] squares = new string[] { "We", "hold", "these", "truths", "to", "be", "self-evident", "that", "all", "men", "are", "create", "equal" };

            Parallel.ForEach(squares, i => Console.WriteLine(string.Format("{0} has {1} letters", i, i.Length)));

            Console.ReadKey();
        }
    }
}

輸出:

hold has 4 letters
We has 2 letters
to has 2 letters
be has 2 letters
self-evident has 12 letters
all has 3 letters
men has 3 letters
are has 3 letters
create has 6 letters
equal has 5 letters
these has 5 letters
truths has 6 letters
that has 4 letters

可以看出,這兩個迴圈在操作的時候,每次迭代所輸出的順序都不一樣,因為他們是並行迴圈,也就是將多個迭代放在多核上處理

其他非同步程式設計模式

當委託物件被呼叫時,它呼叫了它的呼叫列表中包含的方法

如果委託物件在呼叫列表中只有一個方法(引用方法),它就可以非同步執行這個方法。委託類有兩個方法,叫做BeginInvoke和EndInvoke,它們就是用來這麼做的

  • 當呼叫委託的BeginInvoke方法,它開始在一個獨立執行緒上執行引用方法,並且立即返回到原始執行緒。原始執行緒可以繼續,而引用方法會線上程池的執行緒中並行執行。
  • 當程式希望獲取已完成的非同步方法的結果時,可以檢查BeginInvoke返回的IAsyncResult的IsCompleted屬性,或呼叫委託的EndInvoke該來 等等委託完成
  • 等等——完成模式中,在發起了非同步方法以及做了一些其他處理之後,原始執行緒就中斷並且等非同步方法完成之後再繼續
  • 輪詢模式中,原始執行緒定期檢查發起的線和是否完成,如果沒有則可以繼續做一些其他的事情
  • 回撥模式中,原始執行緒一直執行,無需等等或檢查發起的執行緒是否完成。在發起的線和中的引用方法完成之後,發起的執行緒就會呼叫回撥方法,由回撥方法在呼叫EndInvoke之前處理非同步方法的結果

BeginInvoke和EndInvlke

BeginInvoke的使用

  • 在使用BeginInvoke時,引數列表中的實際引數組成如下:
    • 引用方法需要的引數
    • 兩個額外的引數——callbak引數和state引數
  • BeginInvoke從執行緒池中獲取一個執行緒並且讓引用方法在新的執行緒中開始執行
  • BeginInvoke返回給呼叫執行緒一個實現IAsyncResult介面的物件的引用。這個介面引用包含了線上程池執行緒中執行的非同步方法的當前狀態,原始執行緒然後可以繼續執行

示例:

using System;
using System.Threading.Tasks;

namespace CodeForAsync
{
    delegate long MyDel(int first, int second);

    class Program
    {
        static long Sum(int x, int y)
        {
            int z = x + y;
            return (long)z;
        }
        static void Main(string[] args)
        {
            MyDel del = new MyDel(Sum);
            IAsyncResult iar = del.BeginInvoke(3, 5, null, null);
            // 3, 5是引用方法所需要的引數
            // null, null分別表示回撥方法與狀態沒有值
            // 此時從執行緒池獲取一個一執行緒,並且在新的執行緒上執行Sum方法
            // 收集新執行緒的狀態返回給呼叫者

            Console.ReadKey();
        }
    }
}

EndInvoke的使用

EndInvlke方法用來獲取由非同步方法呼叫返回的值,並且執行緒使用的資源,其特性如下:

  • 它接受一個由BeginInvoke方法返回的IAsyncResult物件的引用,並找到它關聯的執行緒
  • 如果執行緒池的執行緒已經退出,EndInvlke做如下的事情
    • 它清理退出執行緒的狀態並且釋放其資源
    • 它找到引用方法返回的值並且把它作為返回值
  • 如果當EndInvlke被呼叫時執行緒池的執行緒仍然在執行,呼叫執行緒就會停止並等等,直到清理完畢並返回值。因為EndInvlke是為開戶的執行緒進行清理,所以必須確保對每一個BeginInvlke都呼叫EndInvlke
  • 如果非同步方法觸發了非同步,在呼叫EndInvlke時會丟擲異常

示例:

using System;
using System.Threading.Tasks;

namespace CodeForAsync
{
    delegate long MyDel(int first, int second);

    class Program
    {
        static long Sum(int x, int y)
        {
            int z = x + y;
            return (long)z;
        }
        static void Main(string[] args)
        {
            MyDel del = new MyDel(Sum);
            IAsyncResult iar = del.BeginInvoke(3, 5, null, null);

            //必須將IAsyncResult物件作為引數
            //如果委託引用的方法有ref和out引數,也必須包含在EndInvoke引數列表中
            long result = del.EndInvoke(iar);

            Console.ReadKey();
        }
    }
}

來看一個完整的示例:

using System;
using System.Threading;
using System.Threading.Tasks;

namespace CodeForAsync
{
    delegate long MyDel(int first, int second);

    class Program
    {
        static long Sum(int x, int y)
        {
            Console.WriteLine("                   Inside Sum");
            Thread.Sleep(100);

            return x + y;
        }
        static void Main(string[] args)
        {
            MyDel del = new MyDel(Sum);

            Console.WriteLine("Before BeginInvoke");
            IAsyncResult iar = del.BeginInvoke(3, 5, null, null);  // 開始非同步呼叫 
            Console.WriteLine("After BeginInvoke");

            Console.WriteLine("Ding Stuff");  // 在這裡可以做點其他事情

            long result = del.EndInvoke(iar);  // 等等結束並獲取結果
            Console.WriteLine("After EndInvoke: {0}",result);

            Console.ReadKey();
        }
    }
}

輸出:

Before BeginInvoke
After BeginInvoke
Ding Stuff
                   Inside Sum
After EndInvoke: 8

AsyncResult類

  • 當呼叫委託物件的BeginInvoke方法時,系統建立了一個AsyncResult類的物件。然而,它不返回類物件的引用,而是返回物件中包含的IAsyncResult介面的引用
  • AsyncResult物件包含一個叫做AsyncDelegate的屬性,它返回一個指向被呼叫來開戶非同步方法的委託的引用。但是,這個屬性是類物件的一部分而不是介面的一部分
  • 該類還包含一個IsCompleted屬性,返回一個布林值,表示方法是否完成
  • 該類還包含一個AsyncState屬性,返回一個物件的引用,作為BeginInvoke方法呼叫時的state引數。它返回object型別的引用

輪詢模式

在輪詢模式中,原始執行緒發起了非同步方法的呼叫,做一些其他處理,然後使用IAsyncResult物件的IsComplete屬性來定期檢查開戶的執行緒是否完成。如果非同步方法已經完成 ,原始執行緒就呼叫EenInvoke並繼續。否則,它做一些其他處理,然後過一會再檢查

示例:

using System;
using System.Threading;
using System.Threading.Tasks;

namespace CodeForAsync
{
    delegate long MyDel(int first, int second);

    class Program
    {
        static long Sum(int x, int y)
        {
            Console.WriteLine("                   Inside Sum");
            Thread.Sleep(100);

            return x + y;
        }
        static void Main(string[] args)
        {
            MyDel del = new MyDel(Sum);

            Console.WriteLine("Before BeginInvoke");
            IAsyncResult iar = del.BeginInvoke(3, 5, null, null);  // 開始非同步呼叫 
            Console.WriteLine("After BeginInvoke");

            // 輪詢
            while (!iar.IsCompleted)
            {
                Console.WriteLine("Not Done");

                //繼續處理
                for (int i = 0; i < 10000; i++) ;
            }

            Console.WriteLine("Dnoe");  

            long result = del.EndInvoke(iar);  // 等等結束並獲取結果
            Console.WriteLine("Result: {0}",result);

            Console.ReadKey();
        }
    }
}

回撥模式

回撥模式中,一旦初始執行緒發起了非同步方法,它就只管自己,不再考慮同步。當非同步方法呼叫結束之後,系統呼叫一個使用者自定義的方法來處理結果,並且呼叫委託的EndInvoke方法。這個使用者自定義的方法叫做回撥方法回撥

語法示例:

IAsyncResult iar = del.BeginInvoke(3, 5, new AsyncCallback(CallWhenDone), null);
// or
IAsyncResult iar = del.BeginInvoke(3, 5, CallWhenDone , null);

從示例中瞭解真相:

using System;
using System.Runtime.Remoting.Messaging;  // 呼叫AsyncResult型別
using System.Threading;

namespace CodeForAsync
{
    delegate long MyDel(int first, int second);

    class Program
    {
        static long Sum(int x, int y)
        {
            Console.WriteLine("             Inside Sum");
            Thread.Sleep(100);

            return x + y;
        }

        /// <summary>
        /// 定義一個方法,用作回撥
        /// 這裡想在回撥方法中使用委託物件的EndInvoke來處理非同步方法執行後的輸出
        /// 但是委託物件在主執行緒裡,而非在非同步執行緒裡
        /// 所以,這裡將IAsyncResult物件作為引數
        /// </summary>
        /// <param name="iar"></param>
        static void CallWhenDone(IAsyncResult iar)
        {
            Console.WriteLine("             Inside CallWhenDone");

            //將傳來的委託物件進行向下轉換
            //以獲取非同步狀態
            AsyncResult ar = (AsyncResult)iar;
            MyDel del = (MyDel)ar.AsyncDelegate;

            long result = del.EndInvoke(iar);
            Console.WriteLine("             Thre result is: {0}", result);
        }

        static void Main(string[] args)
        {
            MyDel del = new MyDel(Sum);

            Console.WriteLine("Before BeginInvoke");

            //state引數接收一個object型別物件
            //為了在回撥方法中使用委託del,這裡將del物件作為狀態類傳入
            IAsyncResult iar = del.BeginInvoke(3, 5, new AsyncCallback(CallWhenDone), del);  // 開始非同步呼叫 
            Console.WriteLine("Doing more work in Main");
            Thread.Sleep(500);
            Console.WriteLine("Done with Main. Exiting.");

            Console.ReadKey();
        }
    }
}

輸出:

Before BeginInvoke
Doing more work in Main
             Inside Sum
             Inside CallWhenDone
             Thre result is: 8
Done with Main. Exiting.

計時器

這裡介紹System.Threading名稱空間中的那個Timer類

  • 計時器在每次時間到期之後呼叫回撥方法。回撥方法必須是TimerCallback委託形式的,結構如下所示。它接受了一個object型別作為引數,並且返回型別是void

    void TimerCallback(object state)
    
  • 相關屬性:

    • dueTime:是回撥方法首次被呼叫之前的時間
    • period:是兩次成功呼叫回撥函式之間的時間間隔
    • state: 可以是null或在每次回撥方法執行時機傳入的物件的引用

Timer常用的構造如下:

Timer(TimerCallback callback, object state, uint dueTime, uint period)

示例:

using System;
using System.Runtime.Remoting.Messaging;  // 呼叫AsyncResult型別
using System.Threading;

namespace CodeForAsync
{
    delegate long MyDel(int first, int second);

    class Program
    {
        int TimesCalled = 0;

        void Display(object state)
        {
            Console.WriteLine("{0} {1}",state.ToString(), ++TimesCalled);
        }

        static void Main(string[] args)
        {
            Program p = new Program();

            //引數分別為:回撥方法、狀態資訊、開始前時間、間隔時間
            Timer myTimer = new Timer(p.Display, "Processing timer event", 2000, 1000);
            Console.WriteLine("Timer started.");

            Console.ReadKey();
        }
    }
}