【问题标题】:Deriving from SynchonizationContext从 SynchronizationContext 派生
【发布时间】:2012-07-01 05:45:22
【问题描述】:

简而言之,我实现了一个派生自 SynchronizationContext 的类,以使 GUI 应用程序可以轻松地使用在 GUI 线程以外的线程上引发的事件。我非常感谢 cmets 在我的实施中。具体来说,您有什么建议反对或可能导致我没有预见到的问题吗?我的初步测试已经成功。

长版: 我目前正在开发分布式系统 (WCF) 的业务层,该系统使用回调将事件从服务器传播到客户端。我的设计目标之一是提供可绑定的业务对象(即 INotifyPropertyChanged/IEditableObject 等),以便在客户端轻松使用这些对象。作为其中的一部分,我提供了一个回调接口的实现,它在事件进入时处理它们,更新业务对象,进而引发属性更改事件。因此,我需要在 GUI 线程上引发这些事件(以避免跨线程操作异常)。因此,我尝试提供一个自定义 SynchronizationContext,实现回调接口的类使用它来将事件传播到 GUI 线程。另外,我希望这个实现独立于客户端环境——例如WinForms GUI 应用程序或 ConsoleApp 或其他东西。换句话说,我不想假设静态 SynchronizationContext.Current 可用。因此,我使用 ExecutionContext 作为后备策略。

public class ImplicitSynchronisationContext : SynchronizationContext

{

private readonly ExecutionContext m_ExecContext;
private readonly SynchronizationContext m_SyncContext;


public ImplicitSynchronisationContext()
{
    // Default to the current sync context if available.
    if (SynchronizationContext.Current != null)
    {
        m_SyncContext = SynchronizationContext.Current;
    }
    else
    {
        m_ExecContext = ExecutionContext.Capture();
    }
}


public override void Post(SendOrPostCallback d, object state)
{
    if (m_SyncContext != null)
    {
        m_SyncContext.Post(d, state);
    }
    else
    {
        ExecutionContext.Run(
            m_ExecContext.CreateCopy(),
            (object args) =>
            {
                ThreadPool.QueueUserWorkItem(new WaitCallback(this.Invoker), args);
            },
            new object[] { d, state });
    }
}
public override void Send(SendOrPostCallback d, object state)
{
    if (m_SyncContext != null)
    {
        m_SyncContext.Send(d, state);
    }
    else
    {
        ExecutionContext.Run(
            m_ExecContext.CreateCopy(),
            new ContextCallback(this.Invoker),
            new object[] { d, state });
    }
}


private void Invoker(object args)
{
    Debug.Assert(args != null);
    Debug.Assert(args is object[]);

    object[] parts = (object[])args;

    Debug.Assert(parts.Length == 2);
    Debug.Assert(parts[0] is SendOrPostCallback);

    SendOrPostCallback d = (parts[0] as SendOrPostCallback);

    d(parts[1]);
}

}

【问题讨论】:

  • 多线程难,我们去购物+1(好问题)

标签: c# .net multithreading


【解决方案1】:

不幸的是,你写了一些已经存在的东西。 SynchronizationContext 类正是您所做的。向你的主类添加一个属性,类似于:

    public static SynchronizationContext SynchronizationContext {
        get {
            if (SynchronizationContext.Current == null) {
                SynchronizationContext.SetSynchronizationContext(new SynchronizationContext());
            }
            return SynchronizationContext.Current;
        }
    }

或者使用 AsyncOperationManager.SynchronizationContext,它做的事情完全相同。当然是首选。

【讨论】:

  • 我避免了这个,因为我在某处读到这可能会导致意外行为。 IE。如果您在 WinForms 应用程序设置 SynchronizationContext 之前初始化此属性,它会使用已创建的属性还是创建新属性?我需要做更多的测试。我也会看看 AsyncOperationManager,谢谢。您能否详细说明为什么更喜欢使用它?
  • 最好是因为它已经存在了。当 winforms 程序员调用 Application.Run() 之前调用您的代码时,您的代码也会发生完全相同的意外行为。
  • 经过一番挖掘后,我不敢苟同。是的,UI 线程的 SynchronizationContext 是在调用 Application.Run() 时设置的,但 AsyncOperationManager.SynchonizationContext 也是如此(至少在 WinForms 应用程序中)。 IE。 AsyncOperationManager.SynchronizationContext 在 Application.Run() 中设置为 WindowsSynchronizationContext,因此使用它而不是 SynchronizationContext.Current 实际上并不能解决问题。但是,我并不认为需要在 Application.Run() 之前初始化/使用 SynchronizationContext,因此在大多数情况下,这不是问题。感谢您的反馈。
  • 或者我误解了你的意思?无论如何,我同意我的实现并不能解决您所指的问题。
  • AsyncOperationManager.SynchronizationContext 确保您始终获得非空同步器。它返回 winforms 应用程序中的 winforms 同步器(在调用 Application.Run 之后),这是控制台模式应用程序中的默认同步器。
【解决方案2】:

我认为上面的代码在技术上没有任何问题..

但是,它比真正需要的要复杂得多。没有真正的理由复制 ExecutionContext 并在其中运行操作。这会在调用 ThreadPool.QueueUserWorkItem 时自动发生。详见ExecutionContext的文档:

在应用程序域中,每当传输线程时,都必须传输整个执行上下文。这种情况发生在 Thread.Start 方法进行的传输、大多数线程池操作以及通过 Windows 消息泵进行 Windows 窗体线程封送处理的过程中。

就个人而言,除非确实需要,否则我会放弃跟踪 ExecutionContext,并将其简化为:

public class ImplicitSynchronisationContext : SynchronizationContext
{
    private readonly SynchronizationContext m_SyncContext;

    public ImplicitSynchronisationContext()
    {
        // Default to the current sync context if available.
        m_SyncContext = SynchronizationContext.Current;
    }


    public override void Post(SendOrPostCallback d, object state)
    {
        if (m_SyncContext != null)
        {
            m_SyncContext.Post(d, state);
        }
        else
        {
            ThreadPool.QueueUserWorkItem(_ => d(state));
        }
    }

    public override void Send(SendOrPostCallback d, object state)
    {
        if (m_SyncContext != null)
        {
            m_SyncContext.Send(d, state);
        }
        else
        {
            d(state);
        }
    }
}

【讨论】:

  • 谢谢,但这并不能真正解决我的问题。即使在 Post 方法中 m_SyncContext 为空,我也希望在创建我的类的线程上执行异步调用 - 在您的情况下,您只需将其触发到线程池。还是我在这里遗漏了什么?
  • @user477161:您的代码也这样做了。 “在构造这个的线程中触发方法”的唯一方法是提供某种形式的消息循环来处理编组。请记住,线程始终在运行(除非显式阻塞),因此除非您提供某种方法来轮询该线程中的操作,否则无法将方法推送到调用线程。我已经编写了处理轮询的自定义 SynchronizationContext 实现,但它需要实现您自己的消息泵或某种形式的轮询。
【解决方案3】:

我有点不确定你写这门课的动机。

如果您使用的是 WinForms 或 WPF,它们会提供使用 SynchronizationContext.Current 可用的实现。

如果您在控制台应用程序中,那么您可以控制主线程。你是如何与这个帖子交流的?

如果您正在运行 Windows 消息循环,可能您正在使用 WinForms 或 WPF。

如果您在生产者/消费者队列上等待,您的主线程将是消费事件的线程,因此根据定义,您将在主线程上。

尼克

【讨论】:

  • 我倾向于同意。我在其中 SynchronizationContext.Current 为 null 的 ConsoleApp 中对此进行了测试 - 这证实了我在其他地方读到的内容,即不能保证设置它。但是,就像您说的那样,在 WinForms 和 WPF 中设置了此属性(至少在创建表单或控件之后),因此那里并不是真正的问题。
【解决方案4】:

非常感谢大家的反馈。

Hans Passant 的回答让我改进/改变了我的解决方案。

回顾一下,我的问题本质上是如何从我的 WCF 服务获取异步回调以传播到客户端(WinForms 或 WPF)的 UI 线程,而不需要客户端开发人员进行任何工作。

我已经放弃了上面提供的实现,因为它是多余的。我的服务回调契约的实现现在只是有一个重载的构造函数,它采用 bool synchroniseCallbacks。当它为真时,我存储对 AsyncOperationManager.SynchronizationContext 的引用。当事件来自我的服务时,我会使用该同步上下文发布或发送它们。

正如 Hans 指出的,如果使用 AsyncOperationManager 公开的同步上下文的好处是它永远不会为空,而且在 WinForms 和 WPF 等 GUI 应用程序中,它将返回 UI 线程的同步上下文 - 问题已解决!

干杯!

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2012-05-27
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2019-05-20
    • 2013-10-18
    相关资源
    最近更新 更多