我试图在这里发布一个没有代码的精简问题,因为我的问题是如此具体:是否可以/可以在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线程交叉的地方...
谢谢。
答案 0 :(得分: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
Invoke
将DataReceived?.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;
}
答案 1 :(得分:0)
通过反应式扩展,似乎可以更好地解决这种情况: