問題概述
確保一段代碼在主線程中執(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 Task
s).
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:
-
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. -
TaskScheduler
- not a bad approach; it's what I used for a long time before switching toIProgress<T>
. It doesn't force the UI/ViewModel update code out of the background operation logic, though. -
SynchronizationContext
- same advantages and disadvantages toTaskScheduler
, via a lesser-known API. -
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, usingDispatcher
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
).