【发布时间】:2019-01-03 01:43:10
【问题描述】:
我正在使用一些现有代码并尝试将 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 是错误。我认为在幕后都是同一个原因。
这里发生了什么?似乎有一些基本的东西我不明白。
我没有延迟,而是尝试在ColorAsync 中添加await Task.Run()(显示已注释掉),有趣的是这也会引发。为什么?这让我觉得OnPaint 不想等待任何事情,就像存在上下文切换问题一样。但它不介意在PaintAsync 上等待。或者在PaintAsync 上等待ColorAsync。但如果我在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,我正在创建一个保证保持打开状态的新图形上下文...
【问题讨论】:
-
评论不用于扩展讨论;这个对话是moved to chat。
-
是的,Edit2 给出了解释。但这不是一个好的解决方案。不要试图让 Windows 异步,这是几百万行向后兼容的单线程(和一心一意)代码。
标签: c# asynchronous async-await