У меня есть класс LogWriter, который принимает путь к файлу и System.Windows.Forms.TextBox, а затем записывает в оба метода public void Write(string progress). Он использует StreamWriter с AutoFlush для добавления к файлу и, следовательно, сам реализует IDisposable для удаления потока. Он просто каждый раз устанавливает свойство Text в текстовом поле.
У меня есть метод в другом классе, который порождает процессы и использует обработчики событий ErrorDataReceived и OutputDataReceived для вызова Report() в экземпляре Progess.
В обработчике событий нажатия асинхронной кнопки в форме я делаю следующее:
using(LogWriter logger = new LogWriter(filePath, txtProgress))
{
IProgress<string> progress = new Progress<string>(s => logger.Write(s));
await Task.Run(() => this._C.RunProcess(progress));
} //disposal point
До этого я реализовал это с помощью ContinueWith в обработчике синхронизации:
LogWriter logger = new LogWriter(filePath, txtProgress);
Task t = Task.Run(() => this._C.RunProcess(progress));
t.ContinueWith((_) => { coreCurrentLogger.Dispose(); }, TaskScheduler.FromCurrentSynchronizationContext());
Я понимаю, что вызов Progress.Report() использует SynchonizationContext.Post() для постановки лямбды в очередь на выполнение через некоторое время. В этом случае я считаю, что SynchonizationContext будет соответствовать потоку пользовательского интерфейса.
В обоих случаях этот код ведет себя так, как я ожидаю. И TextBox, и файл содержат финальную строку, записанную в выходной поток порожденным процессом.
Основываясь на вышеизложенном, существует ли в любой из реализаций состояние гонки, при котором экземпляр LogWriter может быть удален до того, как все Process.Report() лямбды будут выполнены?
Есть ли разница между двумя реализациями? Нужен ли мне ConfigureAwait(true) в первом примере?
LogWriter добавляется к текстовому файлу, поэтому я не хочу открывать и закрывать файл при каждом вызове Report(). Для краткости я опустил содержимое этого класса: конструктор открывает StreamWriter и сохраняет ссылку на TextBox. Метод Write() выполняет ``tb.Text = value` и sw.WriteLine(value).
Итак, это средство записи журналов, которое также занимается пользовательским интерфейсом. Своеобразный. В любом случае, то, что я написал ранее, это то, что я бы сделал лично (помимо использования реального пакета ведения журнала)
Ага. Это просто «быстрый и грязный» способ инкапсуляции логики записи в текстовое поле, чтобы пользователь мог его видеть, и записи той же строки в файл. Мне нужно сделать это полдюжины раз (одновременно), и я не хотел, чтобы C&P везде было одно и то же или ссылался на пакет журналирования. Спасибо за ваш отзыв.
«Нужен ли мне ConfigureAwait(true) в первом примере?» ConfigureAwait(true) — нет операции. Добавление его в любой код никогда не изменит его поведение.
Я понял, что мне нужно, чтобы удаление происходило в потоке пользовательского интерфейса, и ConfigureAwait(true) сделал бы это. Я забыл, что это будет поведение по умолчанию, и добавление его будет невозможным (фейспалм).





Я понимаю, что вызов
Progress.Report()используетSynchonizationContext.Post()для постановки лямбды в очередь на выполнение через некоторое время. В этом случае я считаю, чтоSynchonizationContextбудет соответствовать потоку пользовательского интерфейса.
Правильный.
Существует ли состояние гонки в любой из реализаций, при котором экземпляр
LogWriterможет быть удален до того, как всеProcess.Report()лямбды будут выполнены?
Да, обе реализации страдают от этого состояния гонки. Класс Progress<T> не предоставляет никакого механизма уведомления о завершении асинхронных Report операций, поэтому, пока вы используете этот класс, условия гонки неизбежны. Вы можете облегчить это состояние гонки, добавив await Task.Yield(); после ожидания задачи Task.Run, надеясь, что базовый SynchonizationContext обрабатывает запланированные операции в порядке FIFO. Хотя в этом нет никакой гарантии. Теоретически возможно, что ожидаемая задача Task.Yield будет выполнена с более высоким приоритетом, минуя существующие запланированные Report операции.
Если вы хотите быть абсолютно уверены, что в вашем коде нет состояния гонки, у вас нет другого выбора, кроме как отказаться от класса Progress<T> и использовать собственную реализацию IProgress<T> . Самый простой — сделать отчетность синхронной, а не асинхронной. Вы можете найти SynchronousProgress<T> здесь. Подразумевается, что фоновая операция будет приостанавливаться каждый раз, когда выдается Report, поэтому она станет немного медленнее. Пока Report выдается нечасто, влияние на производительность не будет достаточно значительным, чтобы быть заметным.
Синхронизация прогресса достигается за счет замедления базовой операции, потенциально значительного. А если базовая операция является асинхронной и вызывает Report в текущем контексте синхронизации (ожидая, что он не заблокируется), это может даже привести к взаимоблокировкам. Если обработчики просто отслеживают, сколько из них запущено, а не блокируют их все Report, вы можете включить способ ожидания всех запущенных в данный момент обработчиков (которых, скорее всего, будет немного) только в самом конце.
@Servy, конечно, синхронная отчетность не идеальна, но это самое простое решение этой проблемы. Если бы у меня возникла эта проблема, я бы сначала сделал именно это и стал бы искать более сложную альтернативу только в том случае, если влияние на производительность окажется совсем не незначительным.
Риск взаимоблокировок вызывает особое беспокойство, даже если вы можете игнорировать производительность, особенно потому, что это может стать проблемой в будущем из-за изменений в базовой операции, которая вряд ли будет знать или учитывать детали реализации в отчетах о ходе выполнения. Это также довольно распространенное явление, когда отчеты о ходе работы происходят достаточно часто, и я ожидаю, что это вызовет беспокойство.
Другая проблема, связанная с блокировкой, заключается в том, что если поток пользовательского интерфейса блокируется/интенсивно используется несвязанными процессами, это замедляет фактическую работу, а не только отчеты пользовательского интерфейса. Это еще один случай, когда влияние на производительность может быть незначительным при первоначальном тестировании, но становится проблемой из-за изменений в [потенциально] несвязанных частях системы. Производительность этой операции внезапно стала зависеть от всего пользовательского интерфейса.
Синхронная отчетность @Servy определенно открывает сценарии тупиковой ситуации, спасибо за указание на это. Я бы не назвал это особенно тревожным, потому что если произойдет взаимоблокировка, это будет замечено сразу, во время разработки приложения. Это справедливо и для типичных взаимоблокировок синхронизации с асинхронными. Они делают свое присутствие чрезвычайно громким. Маловероятно, что они попадут в производственный код. Разработчик буквально вынужден немедленно устранять проблему, используя любой из доступных обходных путей.
Если только поведение не условно. Например, если первая ожидаемая задача использует ConfigureAwait, но лишь изредка возвращает завершенную задачу, или отчет о ходе выполнения является условным и редко появляется до первой ожидаемой задачи с ConfigureAwait(false). Но да, в обычных ситуациях это будет замечено сразу. Но устранить риск достаточно легко, и я считаю, что это стоит сделать. Особенно, если этот метод отчетности о прогрессе используется ОП во многих местах.
@Servy Лично я сторонник синхронных отчетов по трем причинам: 1. они раскрывают распространенную ошибку слишком частых отчетов. 2. Он предоставляет неправильно написанный код, который без причины блокирует поток пользовательского интерфейса. 3. Это очень просто. Особенно распространен 1-й. Я видел, как слишком много людей заполняли планировщик пользовательского интерфейса, вызывая Report миллион раз подряд, а затем задавались вопросом, почему пользовательский интерфейс не отвечает. Синхронное создание отчетов вынуждает разработчика Report действовать консервативно, иначе операция займет вечность (но пользовательский интерфейс останется отзывчивым).
Я предпочитаю решать проблему напрямую, а не косвенно. Имейте что-то, что специально отслеживает скорость реагирования пользовательского интерфейса и регистрирует длительные сообщения пользовательского интерфейса или чрезмерную длину очереди. Это, а также блокировка потока пользовательского интерфейса или переполнение его сообщениями обычно видно при тестировании даже без синхронного отчета о ходе выполнения. Люди просто игнорируют это.
@Servy, альтернатива синхронным отчетам, не страдающая от проблем Progress<T>, может представлять собой класс, планирующий не более одной асинхронной операции. В случае, если Report вызывается снова до того, как предыдущая ожидающая операция будет запущена в потоке пользовательского интерфейса, старая T ожидающей операции будет заменена новой T без запуска дополнительной асинхронной операции. Результатом станут более точные отчеты о прогрессе при большой нагрузке на пользовательский интерфейс и более снисходительное поведение в случае злоупотреблений Report.
Это не поможет OP, поскольку они специально регистрируют всю информацию, и поэтому пропуск какой-либо ее части неприемлем (в лучшем случае они могут быть пакетными, и одно обновление пользовательского интерфейса может включать информацию из многочисленных отчетов о ходе работы), но это конечно, уместно во многих контекстах, не связанных с этим вопросом, да.
@Servy, извини, я забыл добавить, что эта гипотетическая IProgress<T> реализация будет включать в себя механизм уведомлений вашего TrackableProgress<T> класса. По сути, это будет BoundedTrackableProgress<T> с ограниченной емкостью 1.
Здесь действительно существует состояние гонки. ProgressPost является обработчиками, а не отправляет их, поэтому работнику разрешено продолжить выполнение до того, как отчет о ходе выполнения действительно будет создан, а это означает, что теоретически вся операция может завершиться запуском одного или нескольких обработчиков отчетов о ходе выполнения.
Сам класс Progress не позволяет дождаться завершения всех существующих обработчиков.
Решение состоит в том, чтобы обернуть объект Progress в объект, который отслеживает количество текущих обработчиков и предоставляет Task, чтобы уведомить вас, когда все текущие обработчики завершены.
public class TrackableProgress<T> : IProgress<T>
{
private IProgress<T> nestedProgress;
private int handlersRunning = 0;
private TaskCompletionSource? taskCompletionSource;
public object key = new object();
public TrackableProgress(Action<T> action)
{
nestedProgress = new Progress<T>(Handler);
void Handler(T t)
{
try
{
lock (key)
handlersRunning++;
action(t);
}
finally
{
lock (key)
{
handlersRunning--;
if (handlersRunning == 0)
{
taskCompletionSource?.TrySetResult();
taskCompletionSource = null;
}
}
}
}
}
void IProgress<T>.Report(T value)
{
nestedProgress.Report(value);
}
public Task HandlersFinished()
{
Task? result = null;
lock (key)
{
if (handlersRunning > 0)
{
taskCompletionSource ??= new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
result = taskCompletionSource.Task;
}
}
return result ?? Task.CompletedTask;
}
}
Это позволит вам написать:
using(LogWriter logger = new LogWriter(filePath, txtProgress))
{
TrackableProgress<string> progress = new TrackableProgress<string>(s => logger.Write(s));
await Task.Run(() => this._C.RunProcess(progress));
await progress.HandlersFinished();
}
Это хорошее решение. +1 Вы могли бы рассмотреть возможность создания TrackableProgress<T>IAsyncDisposable и перемещения функциональности HandlersFinished в метод DisposeAsync. Это сделало бы использование класса более простым, хотя и немного более загадочным.
Вы также можете рассмотреть возможность инициализации TaskCompletionSource с помощью параметра TaskCreationOptions.RunContinuationsAsynchronous, иначе продолжение после await progress.HandlersFinished(); может выполняться внутри раздела lock (key). Это может произойти, если await настроен с помощью ConfigureAwait(false).
@TheodorZoulias Спасибо, я забываю об этом варианте больше, чем следовало бы.
Незначительно связано: гарантирует ли SynchonizationContext, являющийся экземпляром WindowsFormsSynchronizationContext, что лямбды Report() будут выполняться в том порядке, в котором они поставлены в очередь? Т.е. Возникает ли у Winforms та же проблема, что описана здесь: stackoverflow.com/questions/33699053
@TheodorZoulias, не могли бы вы объяснить немного больше, почему продолжение «может работать внутри раздела замка (ключа)»? Связано ли это с повторным заказом при оптимизации?
О, потому что TrySetResult() вызывается внутри блокировки, которая может запустить продолжение прямо здесь?
@DanDef да, причина в TrySetResult, который вызывается внутри раздела lock. Нечто подобное произойдет, если вы Cancel найдёте CancellationTokenSource внутри раздела lock. Обратные вызовы, прикрепленные к связанному CancellationToken, вызываются прямо здесь.
Я бы создал объект LogWriter внутри метода, переданного как делегат Action<T>, вызванный
Progress.Report(), вместо использования Lambda, и удалил бы его там, что делает его ~похожим на второй фрагмент кода. Вы пропустили эту часть? который устанавливает TextBox, для краткости, или... -- Если у этого объекта LogWriter есть другие задачи в этом контексте, добавьте подробности