使用async / await时,使用自定义SynchronizationContext序列化执行

时间:2014-01-08 15:25:41

标签: c# .net async-await synchronizationcontext

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

  1. 致电Post时:
    • 如果可以进行锁定,则立即执行委托
    • 如果无法获取锁定,则委托排队
  2. 致电发送时:
    • 如果可以执行锁定,则执行委托
    • 如果无法锁定,则线程被阻止
  3. 在所有情况下,在执行委托之前,上下文将自身设置为当前上下文,并在委托返回时恢复原始上下文。

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

    1. 该模式真的安全吗?
    2. 是否有更好的方法来实现线程同步?
    3. 以下是SerializingSynchronizationContext的源代码和GitHub上的演示。

      以下是它的使用方法:

      • 每个想要保护的类都会像互斥锁一样创建自己的上下文实例。
      • 上下文是可以接受的,因此可以使用以下语句。

        await myContext;

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

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

2 个答案:

答案 0 :(得分:4)

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

当然,拥有单线程同步上下文具有完美的概念意义。表示UI状态的所有同步上下文都是这样的。 winforms,WPF,winphone等同步上下文都确保一次只能运行该上下文中的单个操作。

令人担忧的是:

  

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

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

答案 1 :(得分:2)

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

[已编辑] 要发表评论,这是您的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中,为什么在_queue.Enqueue 已经存在的情况下,使用Task.Run((Action)ProcessQueue)将回调排入队列,然后使用ProcessQueue()从池中占用新线程在另一个池线程的循环中抽取_queue 并调度回调?在这种情况下,Task.Run看起来像是在浪费池线程。