这属于老话题了,这篇文章就算做一个小汇总吧。
例子就比如要修改窗体的Title属性,单线程的话,直接写代码就可以了,比如在Window的Loaded事件中修改Title:
private void Window_Loaded_1(object sender, RoutedEventArgs e)
{
Title = "Mgen";
}
直接用多线程修改的话:
System.Threading.ThreadPool.QueueUserWorkItem(_ => Title = "Mgen");
结果肯定是程序崩溃,抛出InvalidOperationException:The calling thread cannot access this object because a different thread owns it. 这是由于UI线程的数据不能直接被其他线程访问或者修改。
直接使用Task:
Task.Factory.StartNew(() => Title = "Mgen");
本质上也是非法操作,但是上述代码不会引发程序崩溃,同样Title属性也不会被修改。原因是Task的这个异常不会被立即抛出,此时Task的异常处于未觉察状态,这个未觉察状态的异常会在垃圾回收时终结器执行线程中被抛出(更多请参考这篇文章:.NET(C#) TPL:Task中未觉察异常和TaskScheduler.UnobservedTaskException事件)
解决方案之一就是使用WPF的Dispatcher线程模型来修改,BeginInvoke会立即返回,Invoke会等执行完后再返回。
this.Dispatcher.BeginInvoke(new Action(() => Title = "Mgen"));
另一种方法就是使用SynchronizationContext,它的Post和Send方法类似BeginInvoke和Invoke,只不过SynchronizationContext抽象化了UI平台的线程模型,他是在System.Threading命名空间下,可以在Windows Forms和WPF中通用。
不过注意SynchronizationContext的Current属性只有在UI线程下才会为非null,因此需要通过SynchronizationContext.SetSynchronizationContext方法在多线程环境下设置SynchronizationContext.Current属性。那么上面的代码改用SynchronizationContext可以这样写:
//使用SynchronizationContext.SetSynchronizationContext方法在多线程环境下设置SynchronizationContext.Current属性
System.Threading.SynchronizationContext.SetSynchronizationContext(
new System.Windows.Threading.DispatcherSynchronizationContext(App.Current.Dispatcher));
System.Threading.SynchronizationContext.Current.Send(_ => Title = "Mgen", null);
实际上上述代码在WPF下等效于调用Dispatcher.BeginInvoke方法,当然SynchronizationContext在不同UI框架下的执行都是不一样的。
最后TPL中的Task执行也支持SynchronizationContext,通过使用TaskScheduler.FromCurrentSynchronizationContext获取一个和当前SynchronizationContext相关的TaskScheduler,利用这个TaskScheduler,Task将通过当前SynchronizationContext来执行,这样内部调用Dispatcher的相关方法最终Task可以安全访问UI线程的数据。
代码:
Task.Factory.StartNew(() => Title = "Mgen", CancellationToken.None, TaskCreationOptions.None, TaskScheduler.FromCurrentSynchronizationContext());
最后在新的C# 5.0和.NET 4.5(目前还是beta)下,可以使用async/await来更好地简化异步变成,而且await后的代码会自动根据当前SynchronizationContext来执行。比如在另一个线程中执行一些操作后在修改主线程的Title属性。
如下示例代码:
//注意在方法中加async
private async void Window_Loaded_1(object sender, RoutedEventArgs e)
{
//在另一个线程中睡眠500毫秒来模拟做工作
await Task.Run(() => System.Threading.Thread.Sleep(500));
//做完工作后修改主UI线程的数据
Title = "Mgen";
}