从同步上下文派生

3
简而言之,我实现了一个从SynchronizationContext派生的类,使GUI应用程序可以轻松地消费在GUI线程以外的线程上引发的事件。我非常希望对我的实现进行评论。具体来说,有什么建议或可能会引起问题但我没有预见到的东西吗?我的初步测试已经成功。
长话短说: 我目前正在开发一个分布式系统(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(好问题) - leppie
4个回答

2

很遗憾,您写的内容已经存在。SynchronizationContext类正好可以做到您想要的。请在您的主类中添加一个类似于以下代码的属性:

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

或者使用AsyncOperationManager.SynchronizationContext,它做的事情完全相同。当然,这更可取。

我避免这样做是因为我在某个地方读到过,这可能会导致意外的行为。例如,如果在WinForms应用程序设置SynchronizationContext之前初始化此属性,那么它会使用已经创建的还是创建一个新的呢?我需要进行更多的测试。我也会看一下AsyncOperationManager,谢谢。您能否详细说明为什么最好使用它? - jlindqvist75
这是首选,因为它已经存在。当WinForms程序员在调用Application.Run()之前调用您的代码时,您的代码也会出现完全相同的意外行为。 - Hans Passant
经过更深入的挖掘,我不同意了。是的,在调用Application.Run()时设置了UI线程的SynchronizationContext,但在WinForms应用程序中也设置了AsyncOperationManager.SynchonizationContext。也就是说,AsyncOperationManager.SynchronizationContext在Application.Run()中设置为WindowsSynchronizationContext,因此使用它而不是SynchronizationContext.Current实际上并不能解决问题。然而,我真的看不出在Application.Run()之前初始化/使用SynchronizationContext的必要性,因此在大多数情况下这不是一个问题。感谢反馈。 - jlindqvist75
AsyncOperationManager.SynchronizationContext 确保您始终获得非空的同步器。在 WinForms 应用程序中(在调用 Application.Run 后),它返回 WinForms 同步器;在控制台模式应用程序中,它返回默认同步器。 - Hans Passant
刚刚得出了同样的结论,非常感谢您的反馈。只是补充一下,对于 WPF 应用程序也适用,正如您的答案所暗示的并且我刚刚验证过的那样。 - jlindqvist75
显示剩余2条评论

1

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

然而,它比实际需要的更加复杂。没有真正的理由去复制ExecutionContext并在其中运行操作。这会自动发生在调用ThreadPool.QueueUserWorkItem时。有关详细信息,请参阅ExecutionContext的文档:

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

就我个人而言,除非确实需要,否则我会放弃跟踪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为空,我也希望异步调用在创建我的类的线程上执行 - 在您的情况下,您只是将其发送到线程池。或者我错过了什么? - jlindqvist75
@user477161:你的代码也是这样做的。唯一能够“在构造此线程的方法中触发一个方法”的方式是提供某种形式的消息循环来处理封送。请记住,线程始终在运行(除非明确阻塞),因此,除非您提供某种轮询该线程中的操作的手段,否则无法将方法推入调用线程。我编写了自定义的SynchronizationContext实现来处理此问题,但它需要实现自己的消息泵或某种形式的轮询。 - Reed Copsey

0

我对你编写这个类的动机有点不确定。

如果你正在使用WinForms或WPF,则可以使用SynchronizationContext.Current提供的实现。

如果你在控制台应用程序中,则控制主线程。你如何与此线程通信?

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

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

Nick


我倾向于同意。我在ConsoleApp中进行了测试,其中SynchronizationContext.Current为null - 这证实了我在其他地方读到的内容,即它不能保证被设置。然而,就像你所说,在WinForms和WPF中,这个属性是被设置的(至少在创建了一个窗体或控件之后),所以在那里并不是真正的问题。 - jlindqvist75

0

非常感谢大家的反馈。

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

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

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

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

干杯!


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接