1. 程式人生 > >.NET設計篇08-執行緒取消模型和跨執行緒訪問UI

.NET設計篇08-執行緒取消模型和跨執行緒訪問UI

知識需要不斷積累、總結和沉澱,思考和寫作是成長的催化劑,輸出倒逼輸入

內容目錄

一、執行緒統一取消模型1、取消令牌2、可以中斷的執行緒1、設計一箇中斷函式2、建立CancellationTokenSource物件3、啟動執行緒4、取消執行緒執行二、跨執行緒訪問UI基本方法1、Control.Invoke和BeginInvoke2、桌面退出3、編寫執行緒安全的控制元件三、BackgroundWorker元件1、幹活的程式碼2、啟動任務3、結果取回4、取消任務5、進度報告四、等等

一、執行緒統一取消模型

執行緒取消在多執行緒開發中非常普遍,鑑於此微軟在.NET4.0基礎類庫中引入執行緒統一取消模型對執行緒取消功能的支援。

1、取消令牌

執行緒統一取消模型中兩個重要的型別就是CancellationToken和CancellationTokenSource
每個CancellationTokenSource物件都包容著一個“取消令牌(CancellationToken)”,並通過它的Token屬性向外界展露,用法也很簡單通過呼叫CancellationTokenSource的Cancel()方法通知取消令牌。下面有個小栗子

2、可以中斷的執行緒

下面介紹一個可以中斷的執行緒模型流程

1、設計一箇中斷函式

一個可以中斷的執行緒函式模板看起來像下面這樣

public class ThreadFuncObject
{
    //通過建構函式從外界傳入取消令牌
    private CancellationToken _token;
    public ThreadFuncObject(CancellationToken token)
    {
        _token = token;
    }
    public void DoWork()
    {
        //各種功能程式碼...
        if (_token.IsCancellationRequested)
        {
            //完成清理工作...
            //簡單的處理直接return即可
            //return;
            //建議丟擲個取消異常
            throw new OperationCanceledException(_token);
        }
        //各種功能程式碼...
    }
}
2、建立CancellationTokenSource物件
CancellationTokenSource tokenSource = new CancellationTokenSource();
3、啟動執行緒
ThreadFuncObject obj = new ThreadFuncObject(tokenSource.Token);
Thread th = new Thread(obj.DoWork);
th.Start();
4、取消執行緒執行
tokenSource.Cancel();

二、跨執行緒訪問UI基本方法

剛開始桌面程式開發時,在做進度條等通知UI更新的功能時,經常會遇到跨執行緒訪問UI的問題,拋的錯就是控制元件不能從不是建立它的執行緒去更改。

在.NET Framework中,所有的視覺化控制元件都從System.Windows.Forms.Control類派生而來,考慮到跨執行緒訪問控制元件的需要,Control類提供了相應的方法完成跨執行緒更新介面。

1、Control.Invoke和BeginInvoke

//
// 摘要:
//     在擁有此控制元件的基礎視窗控制代碼的執行緒上執行指定的委託。
//
// 引數:
//   method:
//     包含要在控制元件的執行緒上下文中呼叫的方法的委託。
//
// 返回結果:
//     正在被呼叫的委託的返回值,或者如果委託沒有返回值,則為 null。
public object Invoke(Delegate method);

Invoke方法的引數是一個委託,代表在建立控制元件的執行緒中要執行的方法。實際場景中是要向UI傳值的,可以使用下面的過載

//
// 摘要:
//     在擁有控制元件的基礎視窗控制代碼的執行緒上,用指定的自變數列表執行指定委託。
//
// 引數:
//   method:
//     一個方法委託,它採用的引數的數量和型別與 args 引數中所包含的相同。
//
//   args:
//     作為指定方法的引數傳遞的物件陣列。 如果此方法沒有引數,該引數可以是 null。
//
// 返回結果:
//     System.Object,它包含正被呼叫的委託返回值;如果該委託沒有返回值,則為 null。
public object Invoke(Delegate method, params object[] args);

使用像下面這樣

private void ThreadMethod(Object info)
{
    Action<string> del = (msg) => lblInfo.Text = msg;
    lblInfo.Invoke(del,info);
}

Control.Invoke是同步方法,就是當工作執行緒呼叫此方法將一個方法委託給UI執行緒執行以後,它必須等待UI執行緒執行完此方法後才能繼續執行,如果UI執行緒很忙,工作執行緒可能要等待較長的時間不能工作。這可能不太合理

Control.BeginInvoke是非同步方法,就是工作執行緒可以將一個方法傳送給UI執行緒執行之後,繼續執行下一步的任務而無需等待。

UI執行緒是單一的,就是來更新使用者介面和接受使用者響應的。它從訊息佇列中提取訊息處理,如果某個訊息執行時間較長,將會導致介面失去響應,感覺宕機了。所以長時間任務要交給獨立的工作執行緒去執行,UI執行緒只管向用戶展示執行的結果。UI執行緒不應兼職過多

2、桌面退出

桌面(Windows窗體)程式是事件驅動的,應該確保使用者不能頻繁的點選啟動訪問控制元件的執行緒。因為多執行緒同時訪問同一個控制元件,可能會造成程式不穩定,出現意想不到的的結果。可以通過控制按鈕狀態或執行緒狀態,控制執行緒同步。

如果關閉主窗體導致主執行緒退出,工作執行緒還沒結束。因為主窗體銷燬了,其上的控制元件都被銷燬,而工作執行緒還包含著訪問控制元件的程式碼,所以會丟擲ObjectDisposedException異常。

前幾篇執行緒中講到,最簡單的方法就是將工作執行緒設定為後臺執行緒,幫隨著主執行緒的退出而退出,另一個方法就是在窗體FormClosing關閉事件中,判斷工作執行緒的狀態,手動Abort終止它。

3、編寫執行緒安全的控制元件

我們可以將多執行緒訪問功能封裝進控制元件裡,從而簡化編寫跨執行緒訪問時的程式碼。比如Lable標籤控制元件的text屬性不能跨執行緒直接訪問,可以派生出一個新的ThreadSafeLable類,向下面這樣,這樣不管跨不跨執行緒,使用相同的程式碼訪問執行緒安全的控制元件。

public class ThreadSafeLable : Label
{
    //覆蓋基類的Text屬性
    public override string Text
    {
        get
        {
            return base.Text;
        }
        set
        {
            if (InvokeRequired)//跨執行緒呼叫
            {
                Action<string> del = (msg) => base.Text = msg;
                Invoke(del, value);
            }
            else//普通呼叫
            {
                base.Text = value;
            }
        }
    }
}

專案開發中注意封裝一些這樣的控制元件,可以簡化多執行緒呼叫程式碼,提高專案的開發效率

三、BackgroundWorker元件

其實微軟提供了一個現成的元件用於跨執行緒訪問UI的,那就是BackgroundWorker元件,大大簡化了此類程式的開發。基於事件的非同步呼叫模式,就是通過事件告知什麼時候幹什麼事。支援報告進度,支援取消。

1、幹活的程式碼

在DoWork事件中,真正幹活的程式碼放在這裡

2、啟動任務

呼叫BackgroundWorker元件的RunWorkerAsync方法,此方法會激發DoWork事件。此方法有個過載可以傳Object物件,在DoWork中通過DoWorkEventArgs.Argument來接收

3、結果取回

BackgroundWorker元件有一個RunWorkerCompleted事件
其中的RunWorkerCompletedEventArgs引數包含以下重要資訊:

引數屬性描述
e.Result 工作任務執行的結果
e.Error 這是一個Exception物件,如果工作任務執行過程中沒有發生異常,則此屬性為null,如果發生了異常,此屬性引用被丟擲的異常物件
e.Cancelled 如果在工作任務完成前使用者取消了操作,則此屬性為True,否則此屬性為False

4、取消任務

BackgroundWorker元件有一個CancelAsync方法,呼叫此方法將會導致BackgroundWorker元件的只讀屬性CancellationPending為True,然後在DoWork中判斷即可。

private void backgroundWorker1_DoWork(object sender, DoWorkEventArgs e)
{
    BackgroundWorker bw = sender as BackgroundWorker;
    if (bw.CancellationPending)//如果使用者取消了操作
    {
        e.Cancel = true;//此結果將會傳到RunWorkerCompleted事件中
        return;//仍需要手動提交結束任務
    }
    //...
}

5、進度報告

BackgroundWorker元件有一個ProgressChanged事件。在DoWork事件處理程式碼中合適的地方呼叫BackgroundWorker元件的ReportProgress方法,就會激發ProgressChanged事件。ReportProgress除了可以報告進度,也可以通過其過載報告一個object物件,往往是描述資訊。

在ProgressChanged事件中使用執行緒同步上下文做了特殊處理,可以直接訪問窗體上控制元件,無需考慮跨執行緒問題。

四、等等

好多東西以前都認真看過,沒記性就忘了。馬上要搬家了,裝不進腦子裡就帶不走。每次搬家還要為幾本書多付一些搬家費,惆悵。(沒有大家電,書本就是最重的東西了)

給我一杯酒再給我一支菸,說走就走有緣就再見