c# - 为什么“覆盖异步 void OnPaint”中的 Await 会抛出 OutOfMemoryException 或 ArgumentException?
问题描述
我正在使用一些现有代码并尝试将 WinForms 应用程序中的图形系统从顺序转换为并发。到目前为止一切顺利,我已经很好地转换了所有内容,并在整个应用程序中添加了异步任务/等待。这最终导致回到override void OnPaint(PaintEventArgs e)
. 我可以简单地更改void
为async void
,这将使我能够编译并继续我的快乐方式。但是,在运行测试以模拟延迟时遇到了一些我不明白的行为。
请参阅以下代码。
protected override async void OnPaint(PaintEventArgs e)
{
await PaintAsync(e);
}
private async Task<Color> ColorAsync()
{
await Task.Delay(1000); //throws ArgumentException - Parameter is not valid
//Thread.Sleep(1000); //works
Color color = Color.Blue;
//this also throws
//color = await Task<Color>.Run(() =>
//{
// Thread.Sleep(1000);
// return Color.Red;
//});
return color;
}
private async Task PaintAsync(PaintEventArgs e)
{
//await Task.Delay(1000); //throws OutOfMemoryException
//Thread.Sleep(1000); //works
try
{
e.Graphics.SmoothingMode = System.Drawing.Drawing2D.SmoothingMode.AntiAlias;
PointF start = new PointF(121.0F, 106.329636F);
PointF end = new PointF(0.9999999F, 106.329613F);
using (Pen p05 = new Pen(await ColorAsync(), 1F))
{
p05.DashStyle = System.Drawing.Drawing2D.DashStyle.Custom;
p05.DashPattern = new float[] { 4, 2, 1, 3 };
e.Graphics.DrawLine(p05, start, end);
}
base.OnPaint(e);
}
catch(Exception ex)
{
Debug.WriteLine(ex.Message);
}
}
代码中包含的注释以显示有问题的行。 OnPaint()
已经异步,然后等待PaintAsync()
完成。这很重要,因为没有等待它会崩溃。图形上下文将在PaintAsync
完成之前被释放。到目前为止一切顺利,一切都说得通。
为了模拟延迟,然后我将其添加await Task.Delay
到PaintAsync
例程中,以模拟正在完成的一些工作。这会抛出一个OutOfMemoryException
. 为什么?系统没有内存不足。另外,如果相关,我正在编译为 x64。这让我觉得 GDI+ 不喜欢延迟,也许有一个需要访问它的最小阈值,或者 Windows 破坏了句柄?因此,我尝试Thread.Sleep()
查看是否有任何区别。有用。有 1 秒的暂停,然后画线。然后我重复同样的测试,但使用由PaintAsync
,调用的子例程ColorAsync()
。这次ArgumentException
是错误。我认为在幕后都是一样的原因。
这里发生了什么?似乎有一些基本的东西我不明白。
我没有延迟,而是尝试在await Task.Run()
内部添加一个ColorAsync
(显示已注释掉),有趣的是这也会抛出。为什么?这让我觉得OnPaint
不想等待任何事情,就像存在上下文切换问题一样。但它不介意 await on PaintAsync
。或等待ColorAsync
从PaintAsync
. 但如果我等待Task.Run
一个问题?
OnPaint
如果没有这些例外,我该如何等待?
编辑 1
这是一个额外的例子来说明我的困惑并帮助人们理解我在问什么。以下似乎完美无缺。
protected override async void OnPaint(PaintEventArgs e)
{
await PaintAsync2(e);
}
private async Task<Color> ColorAsync()
{
Color color = Color.Blue;
color = await Task<Color>.Run(() =>
{
return Color.Red;
});
return color;
}
private async Task PaintAsync2(PaintEventArgs old)
{
using (Graphics g = CreateGraphics())
{
var e = new PaintEventArgs(g, old.ClipRectangle);
try
{
g.SmoothingMode = System.Drawing.Drawing2D.SmoothingMode.AntiAlias;
PointF start = new PointF(121.0F, 106.329636F);
PointF end = new PointF(0.9999999F, 106.329613F);
using (Pen p05 = new Pen(await ColorAsync(), 1F))
{
p05.DashStyle = System.Drawing.Drawing2D.DashStyle.Custom;
p05.DashPattern = new float[] { 4, 2, 1, 3 };
g.DrawLine(p05, start, end);
}
base.OnPaint(e);
}
catch (Exception ex)
{
Debug.WriteLine(ex.Message);
}
}
}
为什么这行得通,但不是第一个版本?评论表明第一个版本失败了,因为通过等待我将上下文更改为不同的非 UI 线程,从而破坏了传递给 OnPaint() 的图形上下文。所以在第二个版本中,我忽略了传递给 OnPaint 例程的上下文,并执行 CreateGraphics 来建立一个新的图形上下文。但是,正如 MSFT 文档中所述,规则是相同的。
CreateGraphics() 是线程安全的,允许在非 UI 线程上建立图形上下文。但是,只能从创建它的线程访问该上下文。所以,如果等待其他任务导致线程上下文更改,我不应该仍然得到同样的错误吗?ColorAsync 启动一个任务,该任务在单独的线程上运行,返回 Color.Red 并等待它。然后将该颜色传递给图形上下文等。但它可以工作。
为什么一个有效,而另一个无效?另外,用 CreateGraphics() 这样做是不是一个糟糕的设计?即使这是有效的,我是否有一些理由不应该这样做,我会后悔?假设我使用 OnPaint 异步触发一堆后台线程,每个线程负责渲染 UI 的不同方面。每个人都打开自己的图形上下文等。据我所知,这不应该锁定 UI,对吧?在架构上,我可以想到很多我不想这样做的原因。虽然没有深入讨论所有这些细节,但只关注手头的简单原则驱动问题,这有什么问题?
编辑 2
这是我的理论。调用 Control.OnPaint 的代码是什么样的?大概是这样的?
using (Graphics g = CreateGraphics())
{
var clip = new Rectangle(0, 0, this.Width, this.Height);
var args = new PaintEventArgs(g, clip);
if (OnPaint != null)
{
OnPaint(args);
}
}
我所能想到的是,如果 OnPaint 是“async void”,那么它将在完成之前返回,在这种情况下,g.Dispose() 将在使用结束时被调用,然后上下文将被销毁。但是,通过在 PaintAync2 中调用 CreateGraphics,我正在创建一个保证保持打开状态的新图形上下文......
解决方案
我的编辑 2 就是答案。我不知道为什么人们需要上肥皂盒,做出假设,并告诉你你所做的一切都是错误的(即使他们不知道你打算做什么),而不是仅仅回答简单的问题这是被问到的。我所做的只是了解错误的根源,而不是陷入一场大的设计模式辩论。发布的大多数评论都是错误的。Await 没有切换到不同的线程上下文。
在对这些评论感到沮丧之后,我决定查看 System.Windows.Forms.Control 是否有可用的源代码。原来有。我不知道,这是一个巨大的帮助。
看到这个
还有这个
从源头来看,这里是 Control.OnPaint 的调用者
PaintEventArgs 是从 dc 创建的
pevent = new PaintEventArgs(dc, clip);
然后包裹在using()中,也就是说PaintWithErrorHandling返回后会被销毁。PaintWithErrorHandling 就是调用 OnPaint。
using (pevent)
{
try
{
if ((m.WParam == IntPtr.Zero) && GetStyle(ControlStyles.AllPaintingInWmPaint) || doubleBuffered)
{
PaintWithErrorHandling(pevent, PaintLayerBackground);
}
}
...
}
查看 PaintEventArgs 我们看到 Graphics 在访问时被实例化。
public System.Drawing.Graphics Graphics
{
get
{
if (graphics == null && dc != IntPtr.Zero)
{
oldPal = Control.SetUpPalette(dc, false /*force*/, false /*realize*/);
graphics = Graphics.FromHdcInternal(dc);
graphics.PageUnit = GraphicsUnit.Pixel;
savedGraphicsState = graphics.Save(); // See ResetGraphics() below
}
return graphics;
}
}
然后在调用 Dispose 时销毁
protected virtual void Dispose(bool disposing)
{
if (disposing)
{
//only dispose the graphics object if we created it via the dc.
if (graphics != null && dc != IntPtr.Zero)
{
graphics.Dispose();
}
}
....
}
由于覆盖被标记为异步无效,它将在完成之前返回并且图形上下文被破坏。然而,这令人困惑,因为没有等待它不会崩溃,而是使用 Thread.Sleep。这样做的原因是,如果异步函数不等待任何内容,它将同步执行。即使 OnPaint 正在等待 PaintAsync,因为 PaintAsync 并没有等待,一直回到它同步执行的行。这对我来说是一个有趣的启示。我的印象是,标记为 async 且没有等待的函数会同步执行,但仅在该函数的上下文中执行。任何等待此函数的异步函数都会异步执行它,但事实并非如此。
例如,下面的代码。
protected override void OnPaint(PaintEventArgs e)
{
PaintAsync(e);
Debug.WriteLine("done painting");
}
private async Task PaintAsync(PaintEventArgs e)
{
Thread.Sleep(5000);
base.OnPaint(e);
}
Visual Studio 在 PaintAsync 方法下放了一条绿线,上面写着:“此异步方法缺少 'await' 运算符,将同步运行。”
然后在调用 PaintAsync 的 OnPaint 函数中,另一条绿线带有警告,上面写着:“由于未等待此调用,因此在调用完成之前继续执行当前方法。考虑将 'await' 运算符应用于通话的结果。”
根据警告,我希望 PaintAsync(在该函数的上下文中)同步执行。这意味着线程将在等待 Thread.Sleep 时阻塞。然后我希望 OnPaint 在 PaintAsync 完成之前立即返回。然而,这不会发生。一切都像同步一样执行。
以下代码也同步执行。
protected override async void OnPaint(PaintEventArgs e)
{
await PaintAsync(e);
Debug.WriteLine("done painting");
}
private async Task PaintAsync(PaintEventArgs e)
{
Thread.Sleep(5000);
base.OnPaint(e);
}
即使我在 OnPaint 中添加了“async void”和“await”,但一切执行完全相同……因为在任何地方都没有等待新任务。
我会将此称为 Visual Studio 智能感知中的错误。它声明事物将在 OnPaint 中异步执行,但事实并非如此。
推荐阅读
- sas - 如何从 sas 中的混合变量列表(num+char)中仅提取字符值
- java - 如何在导航抽屉中维护片段的回栈?
- kubernetes - 公开应用程序时出现 Kubernetes 入口控制器错误
- java - Play Framework 2.8.x MySQL 连接问题
- javascript - 响应式导航栏菜单(汉堡菜单)无法通过单击打开
- push-notification - push token 的 delegte 方法在 iPhone 5s 和 ios 12.4.5 中没有调用
- python - 如何在 Python 中比较 2 个日期并预测输出
- html - Bootstrap 响应式菜单按钮不会响应
- amazon-ecs - 我可以在同一个 ECS 容器上运行 aws-xray 吗?
- angular - Angular 语言服务无法解析 vscode 中管道调用的签名