我正在使用一些现有代码,并试图将WinForms应用程序中的图形系统从顺序转换为并发。到目前为止,到目前为止,我已经很好地转换了所有内容,并在整个应用程序中添加了异步Task / awaits。这最终导致回到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
不想等待任何事情,例如存在上下文切换问题。但这并不介意在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启动一个Task,该Task在单独的线程上运行,返回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,我创建了一个保证可以保持打开状态的新图形上下文。
答案 0 :(得分:-2)
我的编辑2是答案。我不确定为什么人们需要上肥皂盒,做出假设并告诉您您所做的一切都是错误的(即使他们不知道您打算做什么),而不是仅仅回答一个简单的问题有人问。我要做的只是了解错误的根源,而不是大范围讨论设计模式。发表的大多数评论是错误的。等待未切换到其他线程上下文。
对评论感到沮丧后,我决定看看是否有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,我们看到图形在被访问时被实例化。
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();
}
}
....
}
由于替代标记为async void,它将在完成之前返回,并且图形上下文被破坏。但是,这很令人困惑,因为它没有在没有等待的情况下崩溃,而是在使用Thread.Sleep时崩溃了。这样做的原因是,如果异步函数不等待任何东西,它将同步执行。即使OnPaint正在等待PaintAsync,但由于PaintAsync并未等待,因此一直回溯到它正在同步执行的那一行。这对我来说是一个有趣的启示。我给人的印象是,标记为异步而没有等待的函数将同步执行,但只能在该函数的上下文中执行。任何等待此函数的异步函数都将异步执行它,但这不是事实。
例如,以下代码。
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'运算符,将同步运行。
然后在OnPaint函数内部,调用PaintAsync的另一条绿线显示警告:“由于未等待此调用,因此在调用完成之前将继续执行当前方法。请考虑应用'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”,所有操作的执行方式也完全相同...因为在行的任何地方都没有等待新的Task。
我将其称为Visual Studio智能感知中的错误。它指出事情将在OnPaint中异步执行,但事实并非如此。