Здравствуйте, AK107, Вы писали:
AK>Не понимаю логики поведения сабжа.
Смотрите на task как на некоторое событие — delay отработал, событие выставилось — где-то в другом месте/треде вы его дождались. А то, продолжение будет синхронным или нет это не так важно — исходный task завершается до
Если у Вас нет паранойи, то это еще не значит, что они за Вами не следят.
Re[2]: Объясните поведение ContinueWith(..., TaskContinuationOptions.ExecuteSync
Здравствуйте, TK, Вы писали:
TK>Здравствуйте, AK107, Вы писали:
AK>>Не понимаю логики поведения сабжа.
TK>Смотрите на task как на некоторое событие — delay отработал, событие выставилось — где-то в другом месте/треде вы его дождались. А то, продолжение будет синхронным или нет это не так важно — исходный task завершается до
тогда это не объясняет выполнение первого ContinueWith строго до Task.Wait
Re: Объясните поведение ContinueWith(..., TaskContinuationOptions.ExecuteSynchro
Здравствуйте, AK107, Вы писали:
AK>я не прошу исправить мой пример, чтобы он "заработал". я прошу объяснить мне поведение ContinueWith, а именно отличие первого от второго.
Ох, если совсем коротко, то вот. Если чуть подробней, то лучше не заморачиваться с ContinueWith и реализовать нужное поведение через await. Ну, или хотя бы не терять дочернюю таску, возвращаемую .ContinueWith. В идеале — не увлекаться TaskCreationOptions и не пытаться организовать последовательное выполнение через таймауты или примитивы синхронизации. Ну, в теории На практике, разумеется, все эти советы периодически идут лесом и приходится таки изучать матчасть.
AK>суть вопроса: почему тело первого ContinueWith гарантированно отрабатывает ДО Task.Wait, в том время как второго, нет?
WhenAll() добавляет себя как ContinueWith к задачам-аргументам, поэтому WhenAll выполнится не раньше, чем запустятся предыдущие добавленные продолжения.
Добавите ожидание после последнего Wait(), тогда всё выведется.
Re[4]: Объясните поведение ContinueWith(..., TaskContinuationOptions.ExecuteSync
Здравствуйте, Sinix, Вы писали:
S> WhenAll() добавляет себя как ContinueWith к задачам-аргументам, поэтому WhenAll выполнится не раньше, чем запустятся предыдущие добавленные продолжения.
да, оно! поизучавши исходники net.core пришел к тому же
Re[5]: Объясните поведение ContinueWith(..., TaskContinuationOptions.ExecuteSync
Здравствуйте, AK107, Вы писали:
S>> WhenAll() добавляет себя как ContinueWith к задачам-аргументам, поэтому WhenAll выполнится не раньше, чем запустятся предыдущие добавленные продолжения. AK>да, оно! поизучавши исходники net.core пришел к тому же
Вообще, закладываться на такое поведение опасно, т.к. это просто побочный эффект реализации и никто не запрещает .WhenAll добавляться в начало списка продолжений. Лично я бы переписал код на await-ах или сохранял бы в список запущенные задачи. Все прочие варианты (включая AttachedToParent) ненадёжны.
Здравствуйте, Sinix, Вы писали:
S>Вообще, закладываться на такое поведение опасно, т.к. это просто побочный эффект реализации и никто не запрещает .WhenAll добавляться в начало списка продолжений. Лично я бы переписал код на await-ах или сохранял бы в список запущенные задачи. Все прочие варианты (включая AttachedToParent) ненадёжны.
оценку я не заслужил
вопрос о порядке выполнения возник давеча когда делал custom target для NLog. там такой изврат на самописных continuations для асинхронного логирования да еще и не задокументированный нормально. особый головняк который нужно реализовывать — это FlushAsync, где, как оказалось, нужно дождаться реального завершения всех активных тасков. можно сделать и на await, но придется городить обработку ошибок, тогда как здесь ничего лишнего. вот такое получилось:
в принципе требования удаления из pendingEvents до фактического завершения FlushAsync (через ContinueWith) не критично, но хотелось гарантий на будущее расширение функционала. добавлять же сам ContinueWith в pendingEvents или переносить в тело WriteAsync некошерно, т.к. в этом случае, теоретически, TryRemove может отработать до TryAdd.
AK> private readonly ConcurrentDictionary<AsyncLogEventInfo, Task> pendingEvents = new ConcurrentDictionary<AsyncLogEventInfo, Task>();
AK> protected sealed override void Write(AsyncLogEventInfo info)
AK> {
AK> var task = WriteAsync(info);
//А может быть, что мы здесь переключимся и вызовем FlushAsync?
//Мне кажется или что-то плохое может произойти?
Здравствуйте, AK107, Вы писали:
AK>оценку я не заслужил
Заслужил-заслужил. Народ тут в основном задаёт вопросы и ждёт готового решения. Как по мне, это всё удовольствие от решения проблем портит. Самостоятельно разобраться в не совсем простой теме с докапыванием к матчасти — бесценно
AK>вопрос о порядке выполнения возник давеча когда делал custom target для NLog. там такой изврат на самописных continuations для асинхронного логирования да еще и не задокументированный нормально.
В теории да, но при подмене шедулера — нет. Самое простое решение — складывать в pendingEvents не исходную задачу, а продолжение,
var task = WriteAsync(info).ContinueWith(x =>
{
Task tmp;
pendingEvents.TryRemove(info, out tmp);
});
pendingEvents.TryAdd(info, task);
В теории тут возможна гонка, если Flush() вызовут в момент между запуском задачи и добавлением в словарь. На самом деле эта гонка — часть более глобальной проблемы "что, если Write() вызовут после последнего Flush()" и, как по мне, решать надо именно её, а не частный случай
Ну и в итоге TaskContinuationOptions.ExecuteSynchronously оказывается не нужен вообще.
Для гарантий можно посмотреть на код AsyncTargetWrapper в nLog. Они там не заморачиваются с Flush. Хотя они там вообще ни с чем не заморачиваются. Чередование двух объектов для блокировки (lockObject в AsyncTargetWrapper и SyncRoot в Target) как бы намекает, что ловить тут нечего.
Re[8]: Объясните поведение ContinueWith(..., TaskContinuatio
AK>> private readonly ConcurrentDictionary<AsyncLogEventInfo, Task> pendingEvents = new ConcurrentDictionary<AsyncLogEventInfo, Task>();
AK>> protected sealed override void Write(AsyncLogEventInfo info)
AK>> {
AK>> var task = WriteAsync(info);
SL>//А может быть, что мы здесь переключимся и вызовем FlushAsync?
SL> //Мне кажется или что-то плохое может произойти?
Здравствуйте, Sinix, Вы писали:
S>В теории да, но при подмене шедулера — нет. Самое простое решение — складывать в pendingEvents не исходную задачу, а продолжение, S>
я там выше написал как раз об этом так >>>>добавлять же сам ContinueWith в pendingEvents или переносить в тело WriteAsync некошерно, т.к. в этом случае, теоретически, TryRemove может отработать до TryAdd.
просто давайте представим, что на момент окончания WriteAsync он отработал — да да, например, любая сетевая функция чтения при наличии данных в буфере может быть завершена синхронно и тогда последующий ContinueWith может вызваться сразу. Где гарантии, что продолжение не успеет отработать до TryAdd? тогда получается, что в pendingEvents зависнет задача навсегда, а учитывая объемы прокачиваемые логированием мы получим нормальную такую утечку памяти.
S>В теории тут возможна гонка, если Flush() вызовут в момент между запуском задачи и добавлением в словарь. На самом деле эта гонка — часть более глобальной проблемы "что, если Write() вызовут после последнего Flush()" и, как по мне, решать надо именно её, а не частный случай
не вижу проблем, т.к. Flush каждого target будет вызван дважды самой инфраструктурой NLog при завершении работы приложения.
S>Для гарантий можно посмотреть на код AsyncTargetWrapper в nLog. Они там не заморачиваются с Flush. Хотя они там вообще ни с чем не заморачиваются. Чередование двух объектов для блокировки (lockObject в AsyncTargetWrapper и SyncRoot в Target) как бы намекает, что ловить тут нечего.
я уже вдоль и поперек изучил их target'ы — имхо там все на соплях. а ад на таймерах и не дождаться реального flush во FlushAsync — это бесценно.
в такие моменты дико чешутся руки запилить свой велосипед с блекджеком...
Re[5]: Объясните поведение ContinueWith(..., TaskContinuationOptions.ExecuteSync
Здравствуйте, AK107, Вы писали:
S>> WhenAll() добавляет себя как ContinueWith к задачам-аргументам, поэтому WhenAll выполнится не раньше, чем запустятся предыдущие добавленные продолжения. AK>да, оно! поизучавши исходники net.core пришел к тому же
В изначальных исходниках продолжения добавляемые через whenall вставали в начало списка.
Если у Вас нет паранойи, то это еще не значит, что они за Вами не следят.
Re[9]: Объясните поведение ContinueWith(..., TaskContinuatio
Здравствуйте, AK107, Вы писали:
AK>просто давайте представим, что на момент окончания WriteAsync он отработал
Ага, косяк.
Можно решить в лоб, добавив проверку task.Completed после TryAdd. Мне такой подход не нравится, это явное ad hoc решение. Из опыта, такие костыли обычно лечат симптомы, а не собственно проблему — неявную зависимость между двумя продолжениями. Никаких красивых готовых решений сходу не вспомню, мы обычно просто правим архитектурные косяки, чтобы не заморачиваться с подобной эквилибристикой. Чего умного попадётся — добавлю.
AK>в такие моменты дико чешутся руки запилить свой велосипед с блекджеком...
Охх, в очередной раз радуюсь, что ни в одном проекте нам не пришлось связываться с NLog. Тот ещё монстрик.
Re[9]: Объясните поведение ContinueWith(..., TaskContinuatio
Здравствуйте, AK107, Вы писали:
AK>просто давайте представим, что на момент окончания WriteAsync он отработал — да да, например, любая сетевая функция чтения при наличии данных в буфере может быть завершена синхронно и тогда последующий ContinueWith может вызваться сразу. Где гарантии, что продолжение не успеет отработать до TryAdd? тогда получается, что в pendingEvents зависнет задача навсегда, а учитывая объемы прокачиваемые логированием мы получим нормальную такую утечку памяти.
Сделайте по рабоче-крестьянски — если результат WriteAsync(info) на выходе Completed — ничего не делаем. А если не Completed — начинаем приседать.
Если у Вас нет паранойи, то это еще не значит, что они за Вами не следят.
Re[6]: Объясните поведение ContinueWith(..., TaskContinuationOptions.ExecuteSync
Здравствуйте, TK, Вы писали
S>>> WhenAll() добавляет себя как ContinueWith к задачам-аргументам, поэтому WhenAll выполнится не раньше, чем запустятся предыдущие добавленные продолжения. AK>>да, оно! поизучавши исходники net.core пришел к тому же
TK>В изначальных исходниках продолжения добавляемые через whenall вставали в начало списка.
Поправка — в начало списка встает Task.WaitAll, а вот Task.WhenAll в конец
Если у Вас нет паранойи, то это еще не значит, что они за Вами не следят.
Re[10]: Объясните поведение ContinueWith(..., TaskContinuatio
Здравствуйте, TK, Вы писали:
TK>Сделайте по рабоче-крестьянски — если результат WriteAsync(info) на выходе Completed — ничего не делаем. А если не Completed — начинаем приседать.
с "ничего не делаем" боюсь не получается. все так же нельзя просто взять и добавить в pendingEvents ContinueWith, т.к. Completed может случить в любом месте.
или речь о другом?
имхо только такой вариант с костылями:
protected sealed override void Write(AsyncLogEventInfo info)
{
var task = WriteAsync(info);
pendingEvents.TryAdd(info, task.ContinueWith(x =>
{
Task tmp;
pendingEvents.TryRemove(info, out tmp);
},
TaskContinuationOptions.ExecuteSynchronously);
if (task.IsCompleted)
{
Task tmp;
pendingEvents.TryRemove(info, out tmp);
}
}
Re[11]: Объясните поведение ContinueWith(..., TaskContinuatio
Здравствуйте, AK107, Вы писали:
TK>>Сделайте по рабоче-крестьянски — если результат WriteAsync(info) на выходе Completed — ничего не делаем. А если не Completed — начинаем приседать.
AK>с "ничего не делаем" боюсь не получается. все так же нельзя просто взять и добавить в pendingEvents ContinueWith, т.к. Completed может случить в любом месте.
А зачем его добавлять? Он же уже случился. Если надо гарантированно пролезть в очередь первым — добавьте тот или иной event:
AK>имхо только такой вариант с костылями:
protected sealed override void Write(AsyncLogEventInfo info)
{
var task = WriteAsync(info);
if (!task.IsCompleted)
{
var signal = new TaskCompletionSource<AsyncLogEventInfo>();
task.ContinueWith(t => signal.TrySetResult(info), TaskContinuationOptions.ExecuteSynchronously);
pendingEvents.TryAdd(info, task);
signal.ContinueWith(t => pendingEvents.TryRemove(t), TaskContinuationOptions.ExecuteSynchronously);
}
}
Если у Вас нет паранойи, то это еще не значит, что они за Вами не следят.
Re[12]: Объясните поведение ContinueWith(..., TaskContinuatio
Здравствуйте, TK, Вы писали:
AK>>с "ничего не делаем" боюсь не получается. все так же нельзя просто взять и добавить в pendingEvents ContinueWith, т.к. Completed может случить в любом месте.
TK>А зачем его добавлять? Он же уже случился. Если надо гарантированно пролезть в очередь первым — добавьте тот или иной event:
я о том, что задача может завершиться в момент времени между проверкой task.IsCompleted и TryAdd, поэтому проверка IsCompleted мало что даст до добавления в список — ну может микрооптимизацию
var task = WriteAsync(info);
if (!task.IsCompleted)
{
// типа тут
pendingEvents.TryAdd(info, task);
}
а тут я не понял как нам это поможет во FlushAsync дождаться полного цикла обработки info включая его удаление из pendingEvents — это к вопросу о текущей реализациии порядка срабатывания продолжений в .net о чем вы писали выше. в список так и добавляется оригинальный task, соответственно дожидаться во FlushAsync будут именно его, а продолжения:
protected sealed override void Write(AsyncLogEventInfo info)
{
var task = WriteAsync(info);
if (!task.IsCompleted)
{
var signal = new TaskCompletionSource<AsyncLogEventInfo>();
task.ContinueWith(t => signal.TrySetResult(info), TaskContinuationOptions.ExecuteSynchronously);
pendingEvents.TryAdd(info, task);
signal.ContinueWith(t => pendingEvents.TryRemove(t), TaskContinuationOptions.ExecuteSynchronously);
}
}
цель была: в идеале FlushAsync дожидается не только завершения асинхронной записи info, но и удаления из pendingEvents. изначально я считал, что TaskContinuationOptions.ExecuteSynchronously дает некие гарантии, на что меня поправили — "в текущей реализации, которая может измениться"...