C# WPF程序的線程控制

問題概述

確保一段代碼在主線程中執(zhí)行,可以將代碼包含在通過Dispatcher.Invoke來發(fā)起的Action中,也可以僅通過斷言來在調(diào)試階段發(fā)現(xiàn)問題來減少Dispatcher.Invoke的使用。

基本思路

如果需要讓代碼在主線程執(zhí)行界拦,在WPF程序中可以使用(參考SO的回答):

Dispatcher.Invoke(Delegate, object[])

on the Application's (or any UIElement's) dispatcher.

You can use it for example like this:

Application.Current.Dispatcher.Invoke(new Action(() => { /* Your code here */ }));

or

someControl.Dispatcher.Invoke(new Action(() => { /* Your code here */ }));

這是我一直以來都在用的方式。在編寫代碼的時(shí)候,需要由程序員確信界面相關(guān)的數(shù)據(jù)元素都在Dispatcher中執(zhí)行睡互。

不過這帶來了一個(gè)問題:如果是經(jīng)驗(yàn)不足的程序員,可能會(huì)遺忘這種確定性保證(例如Timer的回調(diào)函數(shù)都是在額外的線程中處理陵像,編碼者很容易疏忽就珠,在這些回調(diào)中處理一些界面元素)。一種方式是在所有需要主線程執(zhí)行的代碼段之外套上Dispatcher.Invoke醒颖,但是很多時(shí)候這不免有些畫蛇添足妻怎。我們其實(shí)只需要一種方式來Assert是否該函數(shù)段總是處于主線程被處理。

同問題的另一個(gè)回答給出了另一個(gè)解決方案泞歉,使用SynchronizationContext
The best way to go about it would be to get a SynchronizationContext from the UI thread and use it. This class abstracts marshalling calls to other threads, and makes testing easier (in contrast to using WPF's Dispatcher directly). For example:

class MyViewModel
{
    private readonly SynchronizationContext _syncContext;

    public MyViewModel()
    {
        // we assume this ctor is called from the UI thread!
        _syncContext = SynchronizationContext.Current;
    }

    // ...

    private void watcher_Changed(object sender, FileSystemEventArgs e)
    {
         _syncContext.Post(o => DGAddRow(crp.Protocol, ft), null);
    }
}

這種方式讓我們?cè)贒ispatcher.Invoke之外有了另一種方法逼侦,可以從子線程中發(fā)起在主線程執(zhí)行的任務(wù)。

結(jié)合這些思路腰耙,我們?cè)诹硪粋€(gè)SO的問題中看到了解決方案:
If you're using Windows Forms or WPF, you can check to see if SynchronizationContext.Current is not null.

The main thread will get a valid SynchronizationContext set to the current context upon startup in Windows Forms and WPF.

解決方案

也就是說偿洁,我們?cè)谀承┍仨氂芍骶€程調(diào)度的函數(shù)起始位置加入如下代碼:
Debug.Assert(SynchronizationContext.Current != null);
這樣可以在代碼中警示開發(fā)人員,必須在調(diào)用時(shí)注意自身的線程上下文沟优。往往通過自動(dòng)化測(cè)試來避免問題涕滋。

另一種思路

You could do it like this:

// Do this when you start your application
static int mainThreadId;

// In Main method:
mainThreadId = System.Threading.Thread.CurrentThread.ManagedThreadId;

// If called in the non main thread, will return false;
public static bool IsMainThread
{
    get { return System.Threading.Thread.CurrentThread.ManagedThreadId == mainThreadId; }
}

EDIT I realized you could do it with reflection too, here is a snippet for that:

public static void CheckForMainThread()
{
    if (Thread.CurrentThread.GetApartmentState() == ApartmentState.STA &&
        !Thread.CurrentThread.IsBackground && !Thread.CurrentThread.IsThreadPoolThread && Thread.CurrentThread.IsAlive)
    {
        MethodInfo correctEntryMethod = Assembly.GetEntryAssembly().EntryPoint;
        StackTrace trace = new StackTrace();
        StackFrame[] frames = trace.GetFrames();
        for (int i = frames.Length - 1; i >= 0; i--)
        {
            MethodBase method = frames[i].GetMethod();
            if (correctEntryMethod == method)
            {
                return;
            }
        }
    }

    // throw exception, the current thread is not the main thread...
}

注意一定要確保靜態(tài)變量實(shí)在主程序入口處的主線程中賦值的。

SO的另一個(gè)問答中使用了 Task-based Asynchronous Pattern

這個(gè)問答的解決方案中也使用了SynchronizationContext挠阁,不過它介紹了另一種重要的技術(shù):IProgress<T>宾肺,這個(gè)技術(shù)也可以用于“測(cè)試友好”的方向來優(yōu)化代碼。

I highly recommend that you read the Task-based Asynchronous Pattern document. This will allow you to structure your APIs to be ready when async and await hit the streets.

I used to use TaskScheduler to queue updates, similar to your solution (blog post), but I no longer recommend that approach.

The TAP document has a simple solution that solves the problem more elegantly: if a background operation wants to issue progress reports, then it takes an argument of type IProgress<T>:

public interface IProgress<in T> { void Report(T value); }

It's then relatively simple to provide a basic implementation:

public sealed class EventProgress<T> : IProgress<T>
{
  private readonly SynchronizationContext syncContext;

  public EventProgress()
  {
    this.syncContext = SynchronizationContext.Current ?? new SynchronizationContext();
  }

  public event Action<T> Progress;

  void IProgress<T>.Report(T value)
  {
    this.syncContext.Post(_ =>
    {
      if (this.Progress != null)
        this.Progress(value);
    }, null);
  }
}

(SynchronizationContext.Current is essentially TaskScheduler.FromCurrentSynchronizationContext without the need for actual Tasks).

The Async CTP contains IProgress<T> and a Progress<T> type that is similar to the EventProgress<T> above (but more performant). If you don't want to install CTP-level stuff, then you can just use the types above.

To summarize, there are really four options:

  1. IProgress<T> - this is the way asynchronous code in the future will be written. It also forces you to separate your background operation logic from your UI/ViewModel update code, which is a Good Thing.
  2. TaskScheduler - not a bad approach; it's what I used for a long time before switching to IProgress<T>. It doesn't force the UI/ViewModel update code out of the background operation logic, though.
  3. SynchronizationContext - same advantages and disadvantages to TaskScheduler, via a lesser-known API.
  4. Dispatcher - really can not recommend this! Consider background operations updating a ViewModel - so there's nothing UI-specific in the progress update code. In this case, using Dispatcher just tied your ViewModel to your UI platform. Nasty.

P.S. If you do choose to use the Async CTP, then I have a few additional IProgress<T> implementations in my Nito.AsyncEx library, including one (PropertyProgress) that sends the progress reports through INotifyPropertyChanged (after switching back to the UI thread via SynchronizationContext).

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末侵俗,一起剝皮案震驚了整個(gè)濱河市锨用,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌隘谣,老刑警劉巖增拥,帶你破解...
    沈念sama閱讀 219,366評(píng)論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件啄巧,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡掌栅,警方通過查閱死者的電腦和手機(jī)秩仆,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,521評(píng)論 3 395
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來猾封,“玉大人澄耍,你說我怎么就攤上這事∩卧担” “怎么了齐莲?”我有些...
    開封第一講書人閱讀 165,689評(píng)論 0 356
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)磷箕。 經(jīng)常有香客問我选酗,道長(zhǎng),這世上最難降的妖魔是什么岳枷? 我笑而不...
    開封第一講書人閱讀 58,925評(píng)論 1 295
  • 正文 為了忘掉前任星掰,我火速辦了婚禮,結(jié)果婚禮上嫩舟,老公的妹妹穿的比我還像新娘氢烘。我一直安慰自己,他們只是感情好家厌,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,942評(píng)論 6 392
  • 文/花漫 我一把揭開白布播玖。 她就那樣靜靜地躺著,像睡著了一般饭于。 火紅的嫁衣襯著肌膚如雪蜀踏。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,727評(píng)論 1 305
  • 那天掰吕,我揣著相機(jī)與錄音果覆,去河邊找鬼。 笑死殖熟,一個(gè)胖子當(dāng)著我的面吹牛局待,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播菱属,決...
    沈念sama閱讀 40,447評(píng)論 3 420
  • 文/蒼蘭香墨 我猛地睜開眼钳榨,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來了纽门?” 一聲冷哼從身側(cè)響起薛耻,我...
    開封第一講書人閱讀 39,349評(píng)論 0 276
  • 序言:老撾萬榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎赏陵,沒想到半個(gè)月后饼齿,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體饲漾,經(jīng)...
    沈念sama閱讀 45,820評(píng)論 1 317
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,990評(píng)論 3 337
  • 正文 我和宋清朗相戀三年缕溉,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了考传。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 40,127評(píng)論 1 351
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡倒淫,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出败玉,到底是詐尸還是另有隱情敌土,我是刑警寧澤,帶...
    沈念sama閱讀 35,812評(píng)論 5 346
  • 正文 年R本政府宣布运翼,位于F島的核電站返干,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏血淌。R本人自食惡果不足惜矩欠,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,471評(píng)論 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望悠夯。 院中可真熱鬧癌淮,春花似錦、人聲如沸沦补。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,017評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽夕膀。三九已至虚倒,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間产舞,已是汗流浹背魂奥。 一陣腳步聲響...
    開封第一講書人閱讀 33,142評(píng)論 1 272
  • 我被黑心中介騙來泰國(guó)打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留易猫,地道東北人耻煤。 一個(gè)月前我還...
    沈念sama閱讀 48,388評(píng)論 3 373
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像准颓,于是被迫代替她去往敵國(guó)和親违霞。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,066評(píng)論 2 355

推薦閱讀更多精彩內(nèi)容