为什么在“覆盖异步void OnPaint”中等待会抛出OutOfMemoryException或ArgumentException?

时间:2019-01-02 15:12:02

标签: c# asynchronous async-await

我正在使用一些现有代码,并试图将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秒,然后画线。然后,我重复相同的测试,但是使用由PaintAsyncColorAsync()调用的子例程。这次ArgumentException是错误。我认为在幕后都是相同的原因。

这是怎么回事?似乎有些根本不了解的事情。

我没有延迟,而是尝试在await Task.Run()内添加一个ColorAsync(显示为注释掉),有趣的是,也抛出了该错误。为什么?这使我认为OnPaint不想等待任何事情,例如存在上下文切换问题。但这并不介意在PaintAsync上等待。或从ColorAsyncPaintAsync等待。但是,如果我在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文档中所述,规则是相同的。

https://docs.microsoft.com/en-us/dotnet/api/system.windows.forms.control.creategraphics?view=netframework-4.7.2

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,我创建了一个保证可以保持打开状态的新图形上下文。

1 个答案:

答案 0 :(得分:-2)

我的编辑2是答案。我不确定为什么人们需要上肥皂盒,做出假设并告诉您您所做的一切都是错误的(即使他们不知道您打算做什么),而不是仅仅回答一个简单的问题有人问。我要做的只是了解错误的根源,而不是大范围讨论设计模式。发表的大多数评论是错误的。等待未切换到其他线程上下文。

对评论感到沮丧后,我决定看看是否有System.Windows.Forms.Control的可用源。原来有。我不知道,这是很大的帮助。

看到这个

https://referencesource.microsoft.com/#system.windows.forms/winforms/managed/system/winforms/control.cs

还有这个

https://referencesource.microsoft.com/#system.windows.forms/winforms/managed/system/winforms/PaintEvent.cs,1b5e97abf89104c1

从源头开始,这是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中异步执行,但事实并非如此。