问题概述
确保一段代码在主线程中执行,可以将代码包含在通过Dispatcher.Invoke来发起的Action中,也可以仅通过断言来在调试阶段发现问题来减少Dispatcher.Invoke的使用。
基本思路
如果需要让代码在主线程执行,在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 */ }));
这是我一直以来都在用的方式。在编写代码的时候,需要由程序员确信界面相关的数据元素都在Dispatcher中执行。
不过这带来了一个问题:如果是经验不足的程序员,可能会遗忘这种确定性保证(例如Timer的回调函数都是在额外的线程中处理,编码者很容易疏忽,在这些回调中处理一些界面元素)。一种方式是在所有需要主线程执行的代码段之外套上Dispatcher.Invoke,但是很多时候这不免有些画蛇添足。我们其实只需要一种方式来Assert是否该函数段总是处于主线程被处理。
同问题的另一个回答给出了另一个解决方案,使用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);
}
}
这种方式让我们在Dispatcher.Invoke之外有了另一种方法,可以从子线程中发起在主线程执行的任务。
结合这些思路,我们在另一个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.
解决方案
也就是说,我们在某些必须由主线程调度的函数起始位置加入如下代码:
Debug.Assert(SynchronizationContext.Current != null);
这样可以在代码中警示开发人员,必须在调用时注意自身的线程上下文。往往通过自动化测试来避免问题。
另一种思路
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...
}
注意一定要确保静态变量实在主程序入口处的主线程中赋值的。
SO的另一个问答中使用了 Task-based Asynchronous Pattern
这个问答的解决方案中也使用了SynchronizationContext,不过它介绍了另一种重要的技术:IProgress<T>,这个技术也可以用于“测试友好”的方向来优化代码。
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
).