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