11

我正在使用 SynchronizationContext 将事件从我的 DLL 编组回 UI 线程,该 DLL 执行大量多线程后台任务。

我知道单例模式不是最喜欢的,但我现在使用它来在创建 foo 的父对象时存储 UI 的 SynchronizationContext 的引用。

public class Foo
{
    public event EventHandler FooDoDoneEvent;

    public void DoFoo()
    {
        //stuff
        OnFooDoDone();
    }

    private void OnFooDoDone()
    {
        if (FooDoDoneEvent != null)
        {
            if (TheUISync.Instance.UISync != SynchronizationContext.Current)
            {
                TheUISync.Instance.UISync.Post(delegate { OnFooDoDone(); }, null);
            }
            else
            {
                FooDoDoneEvent(this, new EventArgs());
            }
        }

    }
}

这在 WPF 中根本不起作用,TheUISync 实例 UI 同步(从主窗口提供)永远不会匹配当前的 SynchronizationContext.Current。在 Windows 窗体中,当我做同样的事情时,它们将在调用后匹配,我们将回到正确的线程。

我讨厌的修复看起来像

public class Foo
{
    public event EventHandler FooDoDoneEvent;

    public void DoFoo()
    {
        //stuff
        OnFooDoDone(false);
    }

    private void OnFooDoDone(bool invoked)
    {
        if (FooDoDoneEvent != null)
        {
            if ((TheUISync.Instance.UISync != SynchronizationContext.Current) && (!invoked))
            {
                TheUISync.Instance.UISync.Post(delegate { OnFooDoDone(true); }, null);
            }
            else
            {
                FooDoDoneEvent(this, new EventArgs());
            }
        }

    }
}

所以我希望这个示例足够有意义。

4

2 回答 2

38

眼前的问题

您的直接问题是SynchronizationContext.Current不会为 WPF 自动设置。要设置它,您需要在 WPF 下运行时在 TheUISync 代码中执行以下操作:

var context = new DispatcherSynchronizationContext(
                    Application.Current.Dispatcher);
SynchronizationContext.SetSynchronizationContext(context);
UISync = context;

更深层次的问题

SynchronizationContext与 COM+ 支持相关联,旨在跨线程。在 WPF 中,您不能拥有跨越多个线程的 Dispatcher,因此SynchronizationContext不能真正跨线程。在许多情况下 aSynchronizationContext可以切换到新线程——特别是任何调用ExecutionContext.Run(). 因此,如果您使用SynchronizationContext为 WinForms 和 WPF 客户端提供事件,则需要注意某些情况会中断,例如对托管在同一进程中的 Web 服务或站点的 Web 请求将是一个问题。

如何绕过需要 SynchronizationContext

因此,我建议Dispatcher专门为此目的使用 WPF 机制,即使使用 WinForms 代码也是如此。您已经创建了一个存储同步的“TheUISync”单例类,因此显然您有一些方法可以连接到应用程序的顶层。无论您这样做,您都可以添加代码来创建将一些 WPF 内容添加到您的 WinForms 应用程序,以便Dispatcher工作,然后使用Dispatcher我在下面描述的新机制。

使用 Dispatcher 而不是 SynchronizationContext

WPF 的Dispatcher机制实际上消除了对单独SynchronizationContext对象的需要。除非您有某些互操作场景,例如与 COM+ 对象或 WinForms UI 共享代码,否则最好的解决方案是Dispatcher使用SynchronizationContext.

这看起来像:

public class Foo 
{ 
  public event EventHandler FooDoDoneEvent; 

  public void DoFoo() 
  { 
    //stuff 
    OnFooDoDone(); 
  } 

  private void OnFooDoDone() 
  { 
    if(FooDoDoneEvent!=null)
      Application.Current.Dispatcher.BeginInvoke(
        DispatcherPriority.Normal, new Action(() =>
        {
          FooDoDoneEvent(this, new EventArgs()); 
        }));
  }
}

请注意,您不再需要 TheUISync 对象 - WPF 会为您处理该细节。

如果您对旧delegate语法更满意,您可以这样做:

      Application.Current.Dispatcher.BeginInvoke(
        DispatcherPriority.Normal, new Action(delegate
        {
          FooDoDoneEvent(this, new EventArgs()); 
        }));

一个不相关的错误要修复

另请注意,此处复制的原始代码中存在错误。问题是 FooDoneEvent 可以在调用 OnFooDoDone 的时间和BeginInvoke(或Post在原始代码中)调用委托的时间之间设置为 null。修复是委托内部的第二个测试:

    if(FooDoDoneEvent!=null)
      Application.Current.Dispatcher.BeginInvoke(
        DispatcherPriority.Normal, new Action(() =>
        {
          if(FooDoDoneEvent!=null)
            FooDoDoneEvent(this, new EventArgs()); 
        }));
于 2010-01-12T16:12:07.607 回答
0

与其和现在的相比,何不让它担心呢?那么这只是处理“无上下文”情况的一种情况:

static void RaiseOnUIThread(EventHandler handler, object sender) {
    if (handler != null) {
        SynchronizationContext ctx = SynchronizationContext.Current;
        if (ctx == null) {
            handler(sender, EventArgs.Empty);
        } else {
            ctx.Post(delegate { handler(sender, EventArgs.Empty); }, null);
        }
    }
}
于 2010-01-12T13:45:04.777 回答