【问题标题】:Changing SynchronizationContext Within Async Method在异步方法中更改 SynchronizationContext
【发布时间】:2019-04-09 23:03:04
【问题描述】:

我想在这里发布一个没有代码的精简问题,因为我的问题非常具体:是否可以/可以接受在 Async 方法中修改 SynchronizationContext 吗?如果我在 Async 方法开始时没有设置 SynchronizationContext,那么其中的代码(包括我引发的事件和我调用的同一个类模块中的方法)似乎在同一个工作线程上运行。但是,当与 UI 交互时,我发现 SynchronizationContext 必须设置为 UI 线程。

是否可以将 SynchronizationContext 设置为工作线程,直到我想要调用基于 UI 的函数?

编辑: 为了进一步澄清我的上述问题,我发现自己在 SynchronizationContext 设置方面处于不利境地。如果不设置 SynchronizationContext 则我的异步操作在单独的线程上运行(根据需要),但是我无法在不遇到跨线程操作异常的情况下将数据返回到 UI 线程;如果我将 SynchronizationContext 设置为我的 UI 线程,那么我想在单独的线程上运行的操作最终会在 UI 线程上运行——然后(当然)我避免了跨线程异常并且一切正常。显然,我错过了一些东西。

如果您想阅读更多内容,我已尝试对我正在尝试做的事情提供一个非常清晰的解释,我知道您正在投入时间来理解,所以谢谢您!

此流程图显示了我正在尝试做的事情:

我有一个在 UI 线程上运行的 Winforms 应用程序(黑色);我有一个 Socket 对象,我想在它自己的线程上运行。套接字类的工作是从通信套接字读取数据,并在数据到达时将事件返回给 UI。

请注意,我从 UI 线程开始了一个消息循环,以便不断轮询套接字在其自己的线程上获取数据;如果接收到数据,我想在从套接字获取更多数据之前在同一个非 UI 线程上同步处理该数据。 (是的,如果任何特定的套接字读取出现问题,套接字中可能会留下未读取的数据。)

启动消息循环的代码如下所示:

if (Socket.IsConnected)
{
   SetUpEventListeners();
   // IMPORTANT: next line seems to be required in order to avoid a cross-thread error
   SynchronizationContext.SetSynchronizationContext(new WindowsFormsSynchronizationContext());
   Socket.StartMessageLoopAsync();
}

当我从 UI 线程启动消息循环时,我在 Socket 对象上调用异步方法:

 public async void StartMessageLoopAsync()
  {
     while (true)
     {
        // read socket data asynchronously and populate the global DataBuffer
        await ReadDataAsync();

        if (DataBuffer.Count == 0)
        {
           OnDataReceived();
        }
     }
  }

Socket 对象也有 OnDataReceived() 方法定义为:

  protected void OnDataReceived()
  {
     var dataEventArgs = new DataEventArgs();
     dataEventArgs.DataBuffer = DataBuffer;

     // *** cross-thread risk here!
     DataReceived?.Invoke(this, dataEventArgs);
  }

我用“1”和“2”蓝色星标突出了图表的两个区域。

在“1”(上图)中,我使用的是异步/等待模式。我正在使用不支持异步的第三方套接字工具,因此我将其包装在自己的 ReadDataAsync() 函数中,如下所示:

  public override async Task ReadDataAsync()
  {
     // read a data asynchronously
     var task = Task.Run(() => ReadData());
     await task;
  }

ReadData() 包装了第三方组件的 read 方法:它填充了一个全局数据缓冲区,因此不需要返回值。

在图中的“2”中,我遇到了上面引用的OnDataReceived() 方法中描述的跨线程风险。

底线:如果我在上面的第一个代码片段中设置SynchronizationContext,那么 Socket 对象中的所有内容都会在自己的线程上运行,直到我尝试调用 DataReceived 事件处理程序;如果我注释掉SynchronizationContext,那么在它自己的线程上运行的代码的唯一部分就是包装在我的DataReadAsync() 方法中的简短的第三方套接字读取操作。

所以我的想法是我是否可以在尝试调用 DataReceived 事件处理程序之前设置SynchronizationContext。即使我“可以”,更好的问题是这是否是一个好主意。如果我确实在非 UI 线程中修改了SynchronizationContext,那么我必须在调用 DataReceived 方法后将其设置回其原始值,这对我来说有一种类似于榴莲的代码气味。

我的设计是否有优雅的调整或是否需要大修?我的目标是让图表中的所有红色项目在非 UI 线程上运行,黑色项目在 UI 线程上运行。 “2”是非 UI 线程我跨到 UI 线程的点...

谢谢。

【问题讨论】:

  • 一切皆有可能,但这个问题似乎有点……嗯……可疑。有很多机制可以回发到 UI。最简单的是异步等待模式并让它从 UI 传播,尽管还有更多取决于你在做什么和如何做。这里的实际用例是什么?
  • 是的,我认为需要更多信息。我将发布一个更完整的问题,因为我真正想做的是阻止异步方法返回 UI 线程,直到我特别想要它。当我在异步方法调用之前设置 SynchronizationContext 时,当我调用某些成员时,异步方法最终回到 UI 线程,我不希望它这样做。显然,我需要重新考虑设计。感谢您的参与。+1
  • 好的,我更新了问题。你可能会后悔你问了这个用例...... :)

标签: c# multithreading async-await


【解决方案1】:

直接设置上下文并不是最好的主意,因为在这个线程中偶尔执行的其他功能可能会受到影响。控制async/await 流的同步上下文最自然的方法是使用ConfigureAwait。因此,在您的情况下,我看到了两种实现您想要的选择:

1)ConfigureAwait(false)ReadDataAsync 一起使用

public async void StartMessageLoopAsync()
{
    while (true)
    {
        // read socket data asynchronously and populate the global DataBuffer
        await ReadDataAsync().ConfigureAwait(false);

        if (DataBuffer.Count == 0)
        {
            OnDataReceived();
        }
    }
}

这将在后台线程中等待后恢复所有内容。然后使用 Dispatcher InvokeDataReceived?.Invoke 编组到 UI 线程中:

protected void OnDataReceived()
{
   var dataEventArgs = new DataEventArgs();
   dataEventArgs.DataBuffer = DataBuffer;

   // *** cross-thread risk here!
   Dispatcher.CurrentDispathcer.Invoke(() => { DataReceived?.Invoke(this, dataEventArgs ); });
}

或者2)进行如下逻辑分解:

    public async void StartMessageLoopAsync()
    {
        while (true)
        {
            // read socket data asynchronously and populate the global DataBuffer
            await ProcessDataAsync();

            // this is going to run in UI thread but there is only DataReceived invocation
            if (DataBuffer.Count == 0)
            {
                OnDataReceived();
            }
        }
    }

OnDataReceived 现在很瘦,只做事件触发

    protected void OnDataReceived()
    {
        // *** cross-thread risk here!
        DataReceived?.Invoke(this, dataEventArgs);
    }

这整合了应该在后台线程中运行的功能

    private async Task ProcessDataAsync()
    {
        await ReadDataAsync().ConfigureAwait(false);

        // this is going to run in background thread
        var dataEventArgs = new DataEventArgs();
        dataEventArgs.DataBuffer = DataBuffer;
    }

    public override async Task ReadDataAsync()
    {
        // read a data asynchronously
        var task = Task.Run(() => ReadData());
        await task;
    }

【讨论】:

  • 我赞成你的回答,但我在工作中遇到了一个不相关的技术问题,占用了我所有的时间。整合后,我会将您的回复标记为答案。现在可能要等到下周了——感谢您的耐心等待。
【解决方案2】:

这似乎是一个更好地使用响应式扩展的场景:

Reactive Extensions for .NET

【讨论】:

  • 也许吧,但我真的在尝试使用 async/await 和我自己的编程来解决这个问题。但我会阅读更多关于这些扩展的信息以备将来使用——到目前为止我所看到的看起来非常有趣。谢谢推荐。
  • async-await 是拉,Rx 是推。事件是推送的。
猜你喜欢
  • 2019-11-24
  • 1970-01-01
  • 2015-12-24
  • 2016-04-17
  • 2016-06-16
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2014-06-20
相关资源
最近更新 更多