【问题标题】:Using a custom SynchronizationContext to serialize execution when using async/await使用 async/await 时使用自定义 SynchronizationContext 序列化执行
【发布时间】:2014-01-26 19:04:01
【问题描述】:

我的团队正在 C# 5.0 中使用 async/await 开发一个多线程应用程序。在实现线程同步的过程中,经过多次迭代,我们想出了一个(可能是新颖的?)带有内部锁的新 SynchronizationContext 实现:

  1. 调用 Post 时:
    • 如果可以获取锁,则立即执行委托
    • 如果无法获得锁,则委托排队
  2. 调用发送时:
    • 如果可以获取锁,则执行委托
    • 如果无法获得锁,则线程被阻塞

在所有情况下,在执行委托之前,上下文将自己设置为当前上下文,并在委托返回时恢复原始上下文。

这是一个不寻常的模式,因为我们显然不是第一个编写这样一个应用程序的人,我想知道:

  1. 这种模式真的安全吗?
  2. 有没有更好的方法来实现线程同步?

这是 SerializingSynchronizationContext 的源代码和GitHub 上的演示。

它的使用方法如下:

  • 每个需要保护的类都会创建自己的上下文实例,例如互斥体。
  • 上下文是可等待的,因此以下语句是可能的。

    await myContext;

    这只会导致方法的其余部分在上下文的保护下运行。

  • 类的所有方法和属性都使用这种模式来保护数据。在等待之间,一次只能在上下文上运行一个线程,因此状态将保持一致。当到达等待时,允许下一个计划线程在上下文上运行。
  • 如果需要保持原子性,即使是等待的表达式,自定义 SynchronizationContext 也可以与 AsyncLock 结合使用。
  • 类的同步方法也可以使用自定义上下文进行保护。

【问题讨论】:

  • 这是重新发明了 TPL DataFlow 的 ActionBlock 一点吗?
  • @Andrew 不是真的;它不一定主要用于受 CPU 限制的工作,而这正是 TPL 数据流的用途。它本质上是一种复制 UI 同步上下文的方式。
  • @Servy 是的。仅使用单个线程完成所有工作的想法似乎反映了这一点。
  • @Servy 我不反对...
  • @Noseratio 此自定义上下文没有线程关联。它一次只允许一个线程执行受上下文保护的任何代码,因此它更像 ASP.NET。更关心的是如何使用这个上下文(参见演示代码),即像一种监视器锁,在等待之前和之后自动释放和重新获取锁。对象具有状态并且可能被多个线程接触,因此不安全。目标主要是易于使用和应用的模式。

标签: c# .net async-await synchronizationcontext


【解决方案1】:

关于锁的使用。这个问题更适合Code Review,但乍一看我不认为你的SerializingSynchronizationContext.Post 做得很好。尝试在一个紧密的循环中调用它。由于Task.Run((Action)ProcessQueue),您很快就会发现越来越多的ThreadPool 线程在lock (_lock) 上被阻塞,同时等待在ProcessQueue() 中获取它。

[EDITED]为了解决评论,这里是您的current implementation

public override void Post(SendOrPostCallback d, object state)
{
    _queue.Enqueue(new CallbackInfo(d, state));

    bool lockTaken = false;
    try
    {
        Monitor.TryEnter(_lock, ref lockTaken);

        if (lockTaken)
        {
            ProcessQueue();
        }
        else
        {
            Task.Run((Action)ProcessQueue);
        }
    }
    finally
    {
        if (lockTaken)
        {
            Monitor.Exit(_lock);
        }
    }
}

// ...

private void ProcessQueue()
{
    if (!_queue.IsEmpty)
    {
        lock (_lock)
        {
            var outer = SynchronizationContext.Current;
            try
            {
                SynchronizationContext.SetSynchronizationContext(this);

                CallbackInfo callback;
                while (_queue.TryDequeue(out callback))
                {
                    try
                    {
                        callback.D(callback.State);
                    }
                    catch (Exception e)
                    {
                        Console.WriteLine("Exception in posted callback on {0}: {1}", 
                            GetType().FullName, e);                 
                    }
                }
            }
            finally
            {
                SynchronizationContext.SetSynchronizationContext(outer);
            }
        }
    }
}

Post 中,为什么在ProcessQueue() 已经在抽_queue 的情况下用_queue.Enqueue 排队回调然后用Task.Run((Action)ProcessQueue) 从池中占用一个新线程 > 在另一个池线程上循环并调度回调?在这种情况下,Task.Run 在我看来就像在浪费一个池线程。

【讨论】:

  • 我已经尝试了您在几种不同情况下描述的紧密循环,并且无法以考虑到这种情况的任何不良行为结束。像这样手动发帖有点做作。
  • @revane,查看我的编辑。为了进一步讨论,也许您可​​以使用上述测试用例更新您的 GitHub 代码?否则,不用担心,只要您对自己的逻辑感到满意。
【解决方案2】:

拥有一个从不一次运行多个操作的同步上下文当然不是新奇的,而且一点也不差。 Here 你可以看到 Stephen Toub 描述如何在两年前制作一个。 (在这种情况下,它只是用作创建消息泵的工具,实际上听起来可能正是您想要的,但即使不是,您也可以将同步上下文从解决方案中拉出并单独使用。)

当然,拥有一个单线程同步上下文在概念上是完美的。所有代表 UI 状态的同步上下文都是这样的。 winforms、WPF、winphone 等同步上下文都确保一次只运行来自该上下文的单个操作。

令人担忧的是:

在所有情况下,在执行委托之前,上下文将自己设置为当前上下文,并在委托返回时恢复原始上下文。

我会说上下文本身不应该这样做。如果调用者希望这个同步上下文成为当前上下文他们可以设置它。如果他们想将其用于当前上下文以外的其他内容,您应该允许他们这样做。有时您想使用同步上下文而不将其设置为当前上下文来同步对某个资源的访问;在这种情况下,只有专门访问该资源的操作才需要使用此上下文。

【讨论】:

  • 我想说,单线程同步上下文只有在真正需要时才具有完美的概念意义,例如如果工作流对象是 COM STA 对象或 UI 元素。否则,像AspNetSynchronizationContext 这样的东西对我来说更有意义。
  • 将当前上下文设置为自定义上下文正在解决的问题是因为我们需要在受保护代码中发生等待之前设置当前同步上下文,以便编译器将导致继续发布到自定义语境。这可以放在自定义等待者中,真的。它是在上下文中完成的,因此调用者不必这样做,从而简化了使用模式。
猜你喜欢
  • 1970-01-01
  • 2019-05-24
  • 1970-01-01
  • 2021-10-05
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2017-07-13
相关资源
最近更新 更多