从异步方法更新 GUI
Updating GUI from async method
在使用 async/await
创建简单示例的过程中,我发现,一些示例只是说明 Button1_Click
类方法的模式,并直接从 async
方法自由更新 GUI 控件。因此,可以将此视为一种安全机制。但是我的测试代码在 mscorlib.dll
中的 TargetInvocationException
异常上不断崩溃,内部异常如:NullReference
、ArgumentOutOfRange
等。关于堆栈跟踪,一切似乎都指向 WinForms.StatusStrip
标签显示结果(并直接由绑定到按钮事件处理程序的 async
方法驱动)。在访问 GUI 控件时使用旧学校 Control.Invoke
时,崩溃似乎已修复。
问题是:我错过了什么重要的事情吗?异步方法是否与以前用于长期操作的 threads/background 工作人员一样安全,因此 Invoke
是推荐的解决方案?直接从 async
方法驱动 GUI 的代码片段是否错误?
编辑:
对于缺少来源的反对者:
创建一个包含三个按钮和一个包含两个标签的 StatusStrip 的简单表单...
//#define OLDSCHOOL_INVOKE
using System;
using System.Threading.Tasks;
using System.Windows.Forms;
namespace AsyncTests
{
public partial class Form1 : Form
{
public Form1()
{
InitializeComponent();
}
private async void LongTermOp()
{
int delay;
int thisId;
lock (mtx1)
{
delay = rnd.Next(2000, 10000);
thisId = firstCount++;
#if OLDSCHOOL_INVOKE
Invoke(new Action(() =>
#endif
label1Gen.Text = $"Generating first run delay #{thisId} of {delay} ms"
#if OLDSCHOOL_INVOKE
))
#endif
;
++firstPending;
}
await Task.Delay(delay);
lock (mtx1)
{
--firstPending;
#if OLDSCHOOL_INVOKE
Invoke(new Action(() =>
#endif
label1Gen.Text = $"First run #{thisId} completed, {firstPending} pending..."
#if OLDSCHOOL_INVOKE
))
#endif
;
}
}
private async Task LongTermOpAsync()
{
await Task.Run((Action)LongTermOp);
}
private readonly Random rnd = new Random();
private readonly object mtx1 = new object();
private readonly object mtx2 = new object();
private int firstCount;
private int firstPending;
private int secondCount;
private int secondPending;
private async void buttonRound1_Click(object sender, EventArgs e)
{
await LongTermOpAsync();
}
private async void buttonRound2_Click(object sender, EventArgs e)
{
await Task.Run(async () =>
{
int delay;
int thisId;
lock (mtx2)
{
delay = rnd.Next(2000, 10000);
thisId = secondCount++;
#if OLDSCHOOL_INVOKE
Invoke(new Action(() =>
#endif
label2Gen.Text = $"Generating second run delay #{thisId} of {delay} ms"
#if OLDSCHOOL_INVOKE
))
#endif
;
++secondPending;
}
await Task.Delay(delay);
lock (mtx2)
{
--secondPending;
#if OLDSCHOOL_INVOKE
Invoke(new Action(() =>
#endif
label2Gen.Text = $"Second run #{thisId} completed, {secondPending} pending..."
#if OLDSCHOOL_INVOKE
))
#endif
;
}
});
}
private void buttonRound12_Click(object sender, EventArgs e)
{
buttonRound1_Click(sender, e);
buttonRound2_Click(sender, e);
}
private bool isRunning = false;
private async void buttonCycle_Click(object sender, EventArgs e)
{
isRunning = !isRunning;
await Task.Run(() =>
{
while (isRunning)
{
buttonRound12_Click(sender, e);
Application.DoEvents();
}
});
}
}
}
由于您使用的是异步方法,我的猜测是您尝试执行的代码不在 UI 线程上。
看这里:
SO Question
Task
和await
在这方面都不给您任何保证。您需要考虑创建任务的上下文以及发布延续的上下文。
如果您在 winforms 事件处理程序中使用 await
,同步上下文将被捕获,并且继续 returns 返回到 UI 线程(事实上,它几乎在给定的代码块上调用 Invoke
)。但是,如果您只是使用 Task.Run
开始一个新任务,或者您从另一个同步上下文 await
开始,这将不再适用。解决方案是 运行 在您可以从 winforms 同步上下文中获得的正确任务调度程序上继续。
但是,需要注意的是,这并不一定意味着 async
事件会正常运行。例如,Winforms 也将事件用于诸如 CellPainting
之类的事情,它实际上同步地依赖于它们 运行ning。如果您在这种情况下使用 await
,它几乎可以保证不会正常工作 - 继续将仍然发布到 UI 线程,但这并不一定保证安全。例如,假设控件有这样的代码:
using (var graphics = NewGraphics())
{
foreach (var cell in cells)
CellPainting(cell, graphics);
}
在您继续 运行 时,graphics
实例完全有可能已经被处理掉。甚至有可能单元格不再是控件的一部分,或者控件本身不再存在。
同样重要的是,代码可能取决于您的代码更改内容 - 例如,在某些事件中,您在 EventArgs
中设置了一些值以指示例如成功,或者给一些 return 值。同样,这意味着您不能在内部使用 await
- 据调用者所知,该函数只是 return 在您执行 await
的那一刻(除非它同步完成)。
在使用 async/await
创建简单示例的过程中,我发现,一些示例只是说明 Button1_Click
类方法的模式,并直接从 async
方法自由更新 GUI 控件。因此,可以将此视为一种安全机制。但是我的测试代码在 mscorlib.dll
中的 TargetInvocationException
异常上不断崩溃,内部异常如:NullReference
、ArgumentOutOfRange
等。关于堆栈跟踪,一切似乎都指向 WinForms.StatusStrip
标签显示结果(并直接由绑定到按钮事件处理程序的 async
方法驱动)。在访问 GUI 控件时使用旧学校 Control.Invoke
时,崩溃似乎已修复。
问题是:我错过了什么重要的事情吗?异步方法是否与以前用于长期操作的 threads/background 工作人员一样安全,因此 Invoke
是推荐的解决方案?直接从 async
方法驱动 GUI 的代码片段是否错误?
编辑: 对于缺少来源的反对者: 创建一个包含三个按钮和一个包含两个标签的 StatusStrip 的简单表单...
//#define OLDSCHOOL_INVOKE
using System;
using System.Threading.Tasks;
using System.Windows.Forms;
namespace AsyncTests
{
public partial class Form1 : Form
{
public Form1()
{
InitializeComponent();
}
private async void LongTermOp()
{
int delay;
int thisId;
lock (mtx1)
{
delay = rnd.Next(2000, 10000);
thisId = firstCount++;
#if OLDSCHOOL_INVOKE
Invoke(new Action(() =>
#endif
label1Gen.Text = $"Generating first run delay #{thisId} of {delay} ms"
#if OLDSCHOOL_INVOKE
))
#endif
;
++firstPending;
}
await Task.Delay(delay);
lock (mtx1)
{
--firstPending;
#if OLDSCHOOL_INVOKE
Invoke(new Action(() =>
#endif
label1Gen.Text = $"First run #{thisId} completed, {firstPending} pending..."
#if OLDSCHOOL_INVOKE
))
#endif
;
}
}
private async Task LongTermOpAsync()
{
await Task.Run((Action)LongTermOp);
}
private readonly Random rnd = new Random();
private readonly object mtx1 = new object();
private readonly object mtx2 = new object();
private int firstCount;
private int firstPending;
private int secondCount;
private int secondPending;
private async void buttonRound1_Click(object sender, EventArgs e)
{
await LongTermOpAsync();
}
private async void buttonRound2_Click(object sender, EventArgs e)
{
await Task.Run(async () =>
{
int delay;
int thisId;
lock (mtx2)
{
delay = rnd.Next(2000, 10000);
thisId = secondCount++;
#if OLDSCHOOL_INVOKE
Invoke(new Action(() =>
#endif
label2Gen.Text = $"Generating second run delay #{thisId} of {delay} ms"
#if OLDSCHOOL_INVOKE
))
#endif
;
++secondPending;
}
await Task.Delay(delay);
lock (mtx2)
{
--secondPending;
#if OLDSCHOOL_INVOKE
Invoke(new Action(() =>
#endif
label2Gen.Text = $"Second run #{thisId} completed, {secondPending} pending..."
#if OLDSCHOOL_INVOKE
))
#endif
;
}
});
}
private void buttonRound12_Click(object sender, EventArgs e)
{
buttonRound1_Click(sender, e);
buttonRound2_Click(sender, e);
}
private bool isRunning = false;
private async void buttonCycle_Click(object sender, EventArgs e)
{
isRunning = !isRunning;
await Task.Run(() =>
{
while (isRunning)
{
buttonRound12_Click(sender, e);
Application.DoEvents();
}
});
}
}
}
由于您使用的是异步方法,我的猜测是您尝试执行的代码不在 UI 线程上。
看这里: SO Question
Task
和await
在这方面都不给您任何保证。您需要考虑创建任务的上下文以及发布延续的上下文。
如果您在 winforms 事件处理程序中使用 await
,同步上下文将被捕获,并且继续 returns 返回到 UI 线程(事实上,它几乎在给定的代码块上调用 Invoke
)。但是,如果您只是使用 Task.Run
开始一个新任务,或者您从另一个同步上下文 await
开始,这将不再适用。解决方案是 运行 在您可以从 winforms 同步上下文中获得的正确任务调度程序上继续。
但是,需要注意的是,这并不一定意味着 async
事件会正常运行。例如,Winforms 也将事件用于诸如 CellPainting
之类的事情,它实际上同步地依赖于它们 运行ning。如果您在这种情况下使用 await
,它几乎可以保证不会正常工作 - 继续将仍然发布到 UI 线程,但这并不一定保证安全。例如,假设控件有这样的代码:
using (var graphics = NewGraphics())
{
foreach (var cell in cells)
CellPainting(cell, graphics);
}
在您继续 运行 时,graphics
实例完全有可能已经被处理掉。甚至有可能单元格不再是控件的一部分,或者控件本身不再存在。
同样重要的是,代码可能取决于您的代码更改内容 - 例如,在某些事件中,您在 EventArgs
中设置了一些值以指示例如成功,或者给一些 return 值。同样,这意味着您不能在内部使用 await
- 据调用者所知,该函数只是 return 在您执行 await
的那一刻(除非它同步完成)。