在异步方法中更改SynchronizationContext

问题描述 投票:0回答:2

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

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

编辑:为了进一步澄清我的上述问题,我发现自己处于与SynchronizationContext设置相关的无法获胜的情况。如果不设置SynchronizationContext,那么我的异步操作在一个单独的线程上运行(根据需要),但是我不能在没有遇到跨线程操作异常的情况下将数据返回给UI线程;如果我将SynchronizationContext设置为我的UI线程,那么我想在单独的线程上运行的操作最终在UI线程上运行 - 然后(当然)我避免了跨线程异常,一切正常。显然,我错过了一些东西。

如果你想读更多,我试图提供一个非常明确的解释我正在尝试做什么,我意识到你正在花时间去理解,所以谢谢你!

我想要做的是在这个流程图中显示:

enter image description here

我有一个在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”(在图上),我使用async / await模式。我正在使用不支持Async的第三方套接字工具,所以我把它包装在我自己的ReadDataAsync()函数中,如下所示:

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

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

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

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

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

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

谢谢。

c# multithreading async-await
2个回答
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;
    }

0
投票

这似乎是一个更好地服务于反应式扩展的方案:

Reactive Extensions for .NET

© www.soinside.com 2019 - 2024. All rights reserved.