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

C# 使用async/await时使用自定义SynchronizationContext序列化执行,c#,.net,async-await,synchronizationcontext,C#,.net,Async Await,Synchronizationcontext,我的团队正在使用C#5.0中的async/await开发一个多线程应用程序。在实现线程同步的过程中,经过几次迭代后,我们提出了一个(可能是新的?)新的SynchronizationContext实现,该实现带有一个内部锁: 致电邮政局时: 如果可以获取锁,则会立即执行委托 如果无法获取锁,则委托将排队 调用Send时: 如果可以获取锁,则执行委托 如果无法获取锁,则线程被阻塞 在所有情况下,在执行委托之前,上下文将自身设置为当前上下文,并在委托返回时恢复原始上下文 这是一种不寻常的模式,

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

  • 致电邮政局时:
    • 如果可以获取锁,则会立即执行委托
    • 如果无法获取锁,则委托将排队
  • 调用Send时:
    • 如果可以获取锁,则执行委托
    • 如果无法获取锁,则线程被阻塞
  • 在所有情况下,在执行委托之前,上下文将自身设置为当前上下文,并在委托返回时恢复原始上下文

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

  • 这种模式真的安全吗?
  • 有更好的方法实现线程同步吗?
  • 下面是SerializingSynchronizationContext的源代码和上的演示

    下面是它的使用方法:

    • 每个需要保护的类都会创建自己的上下文实例,就像互斥对象一样
    • 上下文是可等待的,因此下面的语句是可能的

      等待myContext

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

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

    拥有一个同步上下文,一次不会运行多个操作,这当然不是什么新鲜事,也不是什么坏事。你可以看到Stephen Toub两年前描述了如何制作一个。(在本例中,它只是用作创建消息泵的工具,听起来它可能正是您想要的,但即使它不是,您也可以从解决方案中提取同步上下文并单独使用。)

    当然,拥有一个单线程同步上下文在概念上是非常有意义的。所有表示UI状态的同步上下文如下所示。winforms、WPF、winphone等同步上下文都确保一次只能运行来自该上下文的单个操作

    令人担忧的是:

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


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

    关于锁的使用。这个问题对你来说更合适,但乍一看,我认为你做得不好。试着在一个紧密的循环中调用它。由于
    Task.Run((Action)ProcessQueue)
    ,当您在
    ProcessQueue()
    中等待获取它时,会有越来越多的
    ThreadPool
    线程在
    lock(\u lock)
    上被阻塞

    [已编辑]为了回应评论,以下是您的:


    Post
    中,当
    ProcessQueue()
    已经在另一个池线程的循环中泵送
    \u队列
    并分派回调时,为什么要用
    Task.Run((Action)ProcessQueue)
    将回调排队,然后用
    任务占用池中的一个新线程?在这种情况下,
    Task.Run
    在我看来像是浪费了一个池线程。

    这是不是有点重新发明了TPL数据流的
    ActionBlock
    ;它不一定主要用于CPU受限的工作,这就是TPL数据流的用途。它本质上是一种复制UI同步上下文的方法。@Servy这是真的。不过,只使用一个线程来完成所有工作的想法似乎反映了这一点。@Servy我并不反对…@noserio此自定义上下文没有线程关联。它一次只允许一个线程执行受上下文保护的任何代码,因此更像ASP.NET。关注点更多的是如何使用这个上下文(参见演示代码),例如,一种监视器锁,在等待前后自动释放并重新获取锁。对象具有状态,可能被多个线程触及,因此不安全。目标主要是一个易于使用和应用的模式。我想说,单线程同步上下文只有在真正需要时才具有完美的概念意义,例如,如果工作流对象是COM STA对象或UI元素。否则,,类似于
    AspNetSynchronizationContext
    的内容对我来说更有意义。将当前上下文设置为自定义上下文所解决的问题是,我们需要在受保护代码中发生等待之前设置当前同步上下文,以便编译器将继续发布到自定义上下文上下文这本可以放在定制等待器中,没错。它是在上下文中完成的,这样调用方就不必这样做,从而简化了使用模式
    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);
                }
            }
        }
    }