C# WPF程序的线程控制

2017-06-20  本文已影响179人  Brent姜

问题概述

确保一段代码在主线程中执行,可以将代码包含在通过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 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).

上一篇 下一篇

猜你喜欢

热点阅读