Почему Await в рамках "переопределить асинхронный void OnPaint" выбрасывает OutOfMemoryException или ArgumentException? - PullRequest
0 голосов
/ 02 января 2019

Я работаю с некоторым существующим кодом и пытаюсь преобразовать графическую систему в приложении 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 не хочет ничего ждать, как, например, существует проблема переключения контекста. Но не против дождаться 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);
        }

    }
}

Почему это работает, но не первая версия? Комментарии предполагают, что первая версия дает сбой, потому что, делая ожидание, я меняю контекст на другой не-пользовательский поток, выдувая графический контекст, переданный в OnPaint (). Поэтому во второй версии я игнорирую контекст, переданный подпрограмме OnPaint, и делаю CreateGraphics, чтобы установить новый графический контекст. Однако, как указано в документации MSFT, правила те же.

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

CreateGraphics () является потокобезопасным, что позволяет устанавливать графический контекст в потоках не-пользовательского интерфейса. Однако доступ к этому контексту возможен только из потока, в котором он был создан. Итак, если ожидание какой-либо другой Задачи вызывает изменение контекста потока, не должен ли я по-прежнему получать ту же ошибку? ColorAsync запускает задачу, которая выполняется в отдельном потоке, который возвращает Color.Red и ожидает его. Затем этот цвет передается в графический контекст и т. Д. Но он работает.

Почему один работает, а другой нет? Кроме того, что-то делает с CreateGraphics () таким образом, плохой дизайн? Хотя это работает, есть ли какая-то причина, по которой я не должен делать этого, о чем я пожалею? Скажем, я использую OnPaint для асинхронного запуска множества фоновых потоков, каждый из которых отвечает за отображение различных аспектов пользовательского интерфейса. Каждый открывает свой собственный графический контекст и т. Д. Насколько я могу судить, это не должно блокировать пользовательский интерфейс, верно? Архитектурно я могу придумать несколько причин, по которым я бы не хотел этого делать. Не вдаваясь во все эти особенности, просто сосредоточившись на простых основанных на принципах вопросах, что с этим не так?

Редактировать 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 () будет вызван в конце использования, а затем контекст будет уничтожен. Однако, вызывая CreateGraphics в PaintAync2, я создаю новый графический контекст, который гарантированно останется открытым ...

1 Ответ

0 голосов
/ 02 января 2019

Мой Edit 2 является ответом.Я не уверен, почему люди должны брать мыльницы, делать предположения и рассказывать вам, как все, что вы делаете, неправильно (даже если они понятия не имеют, что вы собираетесь делать), а не просто отвечать на простой вопросэто просят.Все, что я пытался сделать, это понять причину ошибки, а не вступать в большие дебаты по шаблонам проектирования.Большинство публикуемых комментариев были неправильными.Await не переключался на другой контекст потока.

После разочарования в комментариях я решил посмотреть, есть ли источник для 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);

и затем оборачивается с помощью (), что означает, что после 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();
        }
    }

    ....
}

Поскольку переопределение помечено как 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, который гласит: «В этом асинхронном методе отсутствуют операторы« ожидания »и он будет работать синхронно».

А затем внутри функции 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);
}

Несмотря на то, что я добавил «async void» и «await» в OnPaint, все выполняется точно так же... потому что нигде в будущем не ожидается новой Задачи.

Я бы назвал это ошибкой в ​​intellisense в Visual Studio.В нем говорится, что вещи будут выполняться асинхронно в OnPaint, но это не так.

...