Объясните поведение ContinueWith(..., TaskContinuationOption
От: AK107  
Дата: 02.01.17 11:15
Оценка:
Не понимаю логики поведения сабжа.

Такой пример:

            var task = Task.Delay(TimeSpan.FromSeconds(1));

            Console.WriteLine("Task: Delay started.");

            task.ContinueWith(x =>  // 1
            {
                Thread.Sleep(1000);
                Console.WriteLine("\tTask: ContinueWith started.");
                Thread.Sleep(1000);
                Console.WriteLine("\tTask: ContinueWith done.");
            }, TaskContinuationOptions.ExecuteSynchronously);

            Console.WriteLine("WhenAll: started.");
            var whenAll = Task.WhenAll(task);

            whenAll.ContinueWith(x =>  // 2
            {
                Thread.Sleep(1);   // 3
                Console.WriteLine("\tWhenAll: ContinueWith run.");
                Thread.Sleep(1000);
                Console.WriteLine("\tWhenAll: ContinueWith done.");
            }, TaskContinuationOptions.ExecuteSynchronously);

            Console.WriteLine("WhenAll: Wait started.");
            whenAll.Wait();
            Console.WriteLine("WhenAll: Wait done.");


вывод:

Task: Delay started.
WhenAll: started.
WhenAll: Wait started.
Task: ContinueWith started.
Task: ContinueWith done.
WhenAll: Wait done.



в зависимости от третьей выделенной строки (наличия ее или длительности паузы в ней) вывод может дополняться

WhenAll: ContinueWith run.


но никогда не ожидаемым:

WhenAll: ContinueWith run.
WhenAll: ContinueWith done.



Вопрос: чем эти два ContinueWith отличаются, что поведение такое разное?
Отредактировано 02.01.2017 20:19 AndrewVK . Предыдущая версия .
continuewith tpl
Re: Объясните поведение ContinueWith(..., TaskContinuationOptions.ExecuteSynchro
От: TK Лес кывт.рф
Дата: 02.01.17 13:01
Оценка:
Здравствуйте, AK107, Вы писали:

AK>Не понимаю логики поведения сабжа.


Смотрите на task как на некоторое событие — delay отработал, событие выставилось — где-то в другом месте/треде вы его дождались. А то, продолжение будет синхронным или нет это не так важно — исходный task завершается до
Если у Вас нет паранойи, то это еще не значит, что они за Вами не следят.
Re[2]: Объясните поведение ContinueWith(..., TaskContinuationOptions.ExecuteSync
От: AK107  
Дата: 02.01.17 13:02
Оценка:
Здравствуйте, TK, Вы писали:

TK>Здравствуйте, AK107, Вы писали:


AK>>Не понимаю логики поведения сабжа.


TK>Смотрите на task как на некоторое событие — delay отработал, событие выставилось — где-то в другом месте/треде вы его дождались. А то, продолжение будет синхронным или нет это не так важно — исходный task завершается до


тогда это не объясняет выполнение первого ContinueWith строго до Task.Wait
Re: Объясните поведение ContinueWith(..., TaskContinuationOptions.ExecuteSynchro
От: StatujaLeha на правах ИМХО
Дата: 02.01.17 13:52
Оценка:
Здравствуйте, AK107, Вы писали:

А если так?
            var task = Task.Delay(TimeSpan.FromSeconds(1));

            Console.WriteLine("Task: Delay started.");

            task.ContinueWith(x =>  // 1
            {
                Thread.Sleep(1000);
                Console.WriteLine("\tTask: ContinueWith started.");
                Thread.Sleep(1000);
                Console.WriteLine("\tTask: ContinueWith done.");
            }, TaskContinuationOptions.ExecuteSynchronously);

            Console.WriteLine("WhenAll: started.");
            var whenAll = Task.WhenAll(task);

            var T =  whenAll.ContinueWith(x =>  // 2
            {
                Thread.Sleep(100);   // 3
                Console.WriteLine("\tWhenAll: ContinueWith run.");
                Thread.Sleep(1000);
                Console.WriteLine("\tWhenAll: ContinueWith done.");
            }, TaskContinuationOptions.ExecuteSynchronously);

            Console.WriteLine("WhenAll: Wait started.");
            whenAll.Wait();
            Console.WriteLine("WhenAll: Wait done.");
            T.Wait();
Re[2]: Объясните поведение ContinueWith(..., TaskContinuationOptions.ExecuteSync
От: AK107  
Дата: 02.01.17 17:05
Оценка:
Здравствуйте, StatujaLeha, Вы писали:

SL>А если так?


мне не нужно так

я не прошу исправить мой пример, чтобы он "заработал". я прошу объяснить мне поведение ContinueWith, а именно отличие первого от второго.

пример не такой тривиальный каким кажется на первый взгляд.

и да, я знаю, что под капотом у async/await и task'ов, в смысле как оно вообще работает и устроено.

суть вопроса: почему тело первого ContinueWith гарантированно отрабатывает ДО Task.Wait, в том время как второго, нет?
Re[3]: Объясните поведение ContinueWith(..., TaskContinuationOptions.ExecuteSync
От: Sinix  
Дата: 02.01.17 19:57
Оценка: 7 (2) +1
Здравствуйте, AK107, Вы писали:

AK>я не прошу исправить мой пример, чтобы он "заработал". я прошу объяснить мне поведение ContinueWith, а именно отличие первого от второго.


Ох, если совсем коротко, то вот. Если чуть подробней, то лучше не заморачиваться с ContinueWith и реализовать нужное поведение через await. Ну, или хотя бы не терять дочернюю таску, возвращаемую .ContinueWith. В идеале — не увлекаться TaskCreationOptions и не пытаться организовать последовательное выполнение через таймауты или примитивы синхронизации. Ну, в теории На практике, разумеется, все эти советы периодически идут лесом и приходится таки изучать матчасть.

AK>суть вопроса: почему тело первого ContinueWith гарантированно отрабатывает ДО Task.Wait, в том время как второго, нет?

WhenAll() добавляет себя как ContinueWith к задачам-аргументам, поэтому WhenAll выполнится не раньше, чем запустятся предыдущие добавленные продолжения.

Добавите ожидание после последнего Wait(), тогда всё выведется.
Re[4]: Объясните поведение ContinueWith(..., TaskContinuationOptions.ExecuteSync
От: AK107  
Дата: 02.01.17 21:19
Оценка: 48 (1)
Здравствуйте, Sinix, Вы писали:

S> WhenAll() добавляет себя как ContinueWith к задачам-аргументам, поэтому WhenAll выполнится не раньше, чем запустятся предыдущие добавленные продолжения.

да, оно! поизучавши исходники net.core пришел к тому же
Re[5]: Объясните поведение ContinueWith(..., TaskContinuationOptions.ExecuteSync
От: Sinix  
Дата: 02.01.17 21:27
Оценка:
Здравствуйте, AK107, Вы писали:

S>> WhenAll() добавляет себя как ContinueWith к задачам-аргументам, поэтому WhenAll выполнится не раньше, чем запустятся предыдущие добавленные продолжения.

AK>да, оно! поизучавши исходники net.core пришел к тому же
Вообще, закладываться на такое поведение опасно, т.к. это просто побочный эффект реализации и никто не запрещает .WhenAll добавляться в начало списка продолжений. Лично я бы переписал код на await-ах или сохранял бы в список запущенные задачи. Все прочие варианты (включая AttachedToParent) ненадёжны.


P.S. исходники удобнее смотреть на https://referencesource.microsoft.com/ или https://source.dot.net/ .
Re[6]: Объясните поведение ContinueWith(..., TaskContinuatio
От: AK107  
Дата: 02.01.17 21:56
Оценка:
Здравствуйте, Sinix, Вы писали:

S>Вообще, закладываться на такое поведение опасно, т.к. это просто побочный эффект реализации и никто не запрещает .WhenAll добавляться в начало списка продолжений. Лично я бы переписал код на await-ах или сохранял бы в список запущенные задачи. Все прочие варианты (включая AttachedToParent) ненадёжны.


оценку я не заслужил

вопрос о порядке выполнения возник давеча когда делал custom target для NLog. там такой изврат на самописных continuations для асинхронного логирования да еще и не задокументированный нормально. особый головняк который нужно реализовывать — это FlushAsync, где, как оказалось, нужно дождаться реального завершения всех активных тасков. можно сделать и на await, но придется городить обработку ошибок, тогда как здесь ничего лишнего. вот такое получилось:
        private readonly ConcurrentDictionary<AsyncLogEventInfo, Task> pendingEvents = new ConcurrentDictionary<AsyncLogEventInfo, Task>();

        protected sealed override void Write(AsyncLogEventInfo info)
        {
            var task = WriteAsync(info);

            pendingEvents.TryAdd(info, task);

            task.ContinueWith(x =>
            {
                Task tmp;
                pendingEvents.TryRemove(info, out tmp);
            }, 
            TaskContinuationOptions.ExecuteSynchronously);
        }

        private async Task WriteAsync(AsyncLogEventInfo info)
        {
            try
            {
                var result = await Client.PostAsync(...);

                result.EnsureSuccessStatusCode();

                info.Continuation(null);
            }
            catch (Exception ex)
            {
                info.Continuation(ex);
            }
        }

        protected override void FlushAsync(AsyncContinuation asyncContinuation)
        {
            Task.WhenAll(pendingEvents.Values)
                .ContinueWith(x => asyncContinuation(x.Exception), TaskContinuationOptions.ExecuteSynchronously);
        }

в принципе требования удаления из pendingEvents до фактического завершения FlushAsync (через ContinueWith) не критично, но хотелось гарантий на будущее расширение функционала. добавлять же сам ContinueWith в pendingEvents или переносить в тело WriteAsync некошерно, т.к. в этом случае, теоретически, TryRemove может отработать до TryAdd.

вопрос: разве ExecuteSynchronously в данном решении не дает гарантий, что первый COntinueWith выполнится до второго?

S>P.S. исходники удобнее смотреть на https://referencesource.microsoft.com/ или https://source.dot.net/ .

спасибо!
Отредактировано 02.01.2017 21:58 AK107 . Предыдущая версия .
Re[7]: Объясните поведение ContinueWith(..., TaskContinuatio
От: StatujaLeha на правах ИМХО
Дата: 02.01.17 22:29
Оценка:
Здравствуйте, AK107, Вы писали:

AK>
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?
               //Мне кажется или что-то плохое может произойти?

AK>            pendingEvents.TryAdd(info, task);

AK>            task.ContinueWith(x =>
AK>            {
AK>                Task tmp;
AK>                pendingEvents.TryRemove(info, out tmp);
AK>            }, 
AK>            TaskContinuationOptions.ExecuteSynchronously);
AK>        }

...

AK>        protected override void FlushAsync(AsyncContinuation asyncContinuation)
AK>        {
AK>            Task.WhenAll(pendingEvents.Values)
AK>                .ContinueWith(x => asyncContinuation(x.Exception), TaskContinuationOptions.ExecuteSynchronously);
AK>        }
AK>
Re[7]: Объясните поведение ContinueWith(..., TaskContinuatio
От: Sinix  
Дата: 03.01.17 09:12
Оценка:
Здравствуйте, 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
От: AK107  
Дата: 03.01.17 09:28
Оценка:
Здравствуйте, StatujaLeha, Вы писали:

AK>>
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>               //Мне кажется или что-то плохое может произойти?

AK>>            pendingEvents.TryAdd(info, task);

AK>>            task.ContinueWith(x =>
AK>>            {
AK>>                Task tmp;
AK>>                pendingEvents.TryRemove(info, out tmp);
AK>>            }, 
AK>>            TaskContinuationOptions.ExecuteSynchronously);
AK>>        }
AK>>


нет, все ок, с таким же успехом FlushAsync может быть вызван и здесь:

private async Task WriteAsync(AsyncLogEventInfo info)
        {
            try
            {
                var result = await /* или даже здесь вызывается FlushAsync */  Client.PostAsync(...);

// <- вызывается FlushAsync

                result.EnsureSuccessStatusCode();

                info.Continuation(null);
            }
            catch (Exception ex)
            {
                info.Continuation(ex);
            }
        }


да и вообще где угодно внутри .net реализации любой async функции.

от этого задача не потеряется + в самом NLog предусмотрен двойной вызов FlushAsync по завершении приложения
Re[8]: Объясните поведение ContinueWith(..., TaskContinuatio
От: AK107  
Дата: 03.01.17 09:39
Оценка: +1
Здравствуйте, Sinix, Вы писали:

S>В теории да, но при подмене шедулера — нет. Самое простое решение — складывать в pendingEvents не исходную задачу, а продолжение,

S>
S>            var task = WriteAsync(info).ContinueWith(x =>
S>            {
S>                Task tmp;
S>                pendingEvents.TryRemove(info, out tmp);
S>            });
S>            pendingEvents.TryAdd(info, task);

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
От: TK Лес кывт.рф
Дата: 03.01.17 11:19
Оценка: 24 (1)
Здравствуйте, AK107, Вы писали:

S>> WhenAll() добавляет себя как ContinueWith к задачам-аргументам, поэтому WhenAll выполнится не раньше, чем запустятся предыдущие добавленные продолжения.

AK>да, оно! поизучавши исходники net.core пришел к тому же

В изначальных исходниках продолжения добавляемые через whenall вставали в начало списка.
Если у Вас нет паранойи, то это еще не значит, что они за Вами не следят.
Re[9]: Объясните поведение ContinueWith(..., TaskContinuatio
От: Sinix  
Дата: 03.01.17 16:56
Оценка:
Здравствуйте, AK107, Вы писали:

AK>просто давайте представим, что на момент окончания WriteAsync он отработал

Ага, косяк.
Можно решить в лоб, добавив проверку task.Completed после TryAdd. Мне такой подход не нравится, это явное ad hoc решение. Из опыта, такие костыли обычно лечат симптомы, а не собственно проблему — неявную зависимость между двумя продолжениями. Никаких красивых готовых решений сходу не вспомню, мы обычно просто правим архитектурные косяки, чтобы не заморачиваться с подобной эквилибристикой. Чего умного попадётся — добавлю.

AK>в такие моменты дико чешутся руки запилить свой велосипед с блекджеком...


Охх, в очередной раз радуюсь, что ни в одном проекте нам не пришлось связываться с NLog. Тот ещё монстрик.
Re[9]: Объясните поведение ContinueWith(..., TaskContinuatio
От: TK Лес кывт.рф
Дата: 03.01.17 18:43
Оценка:
Здравствуйте, AK107, Вы писали:

AK>просто давайте представим, что на момент окончания WriteAsync он отработал — да да, например, любая сетевая функция чтения при наличии данных в буфере может быть завершена синхронно и тогда последующий ContinueWith может вызваться сразу. Где гарантии, что продолжение не успеет отработать до TryAdd? тогда получается, что в pendingEvents зависнет задача навсегда, а учитывая объемы прокачиваемые логированием мы получим нормальную такую утечку памяти.


Сделайте по рабоче-крестьянски — если результат WriteAsync(info) на выходе Completed — ничего не делаем. А если не Completed — начинаем приседать.
Если у Вас нет паранойи, то это еще не значит, что они за Вами не следят.
Re[6]: Объясните поведение ContinueWith(..., TaskContinuationOptions.ExecuteSync
От: TK Лес кывт.рф
Дата: 03.01.17 19:05
Оценка: 24 (1)
Здравствуйте, TK, Вы писали

S>>> WhenAll() добавляет себя как ContinueWith к задачам-аргументам, поэтому WhenAll выполнится не раньше, чем запустятся предыдущие добавленные продолжения.

AK>>да, оно! поизучавши исходники net.core пришел к тому же

TK>В изначальных исходниках продолжения добавляемые через whenall вставали в начало списка.


Поправка — в начало списка встает Task.WaitAll, а вот Task.WhenAll в конец
Если у Вас нет паранойи, то это еще не значит, что они за Вами не следят.
Re[10]: Объясните поведение ContinueWith(..., TaskContinuatio
От: AK107  
Дата: 08.01.17 09:16
Оценка: +1
Здравствуйте, 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
От: TK Лес кывт.рф
Дата: 08.01.17 10:32
Оценка:
Здравствуйте, 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
От: AK107  
Дата: 08.01.17 11:39
Оценка:
Здравствуйте, 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 дает некие гарантии, на что меня поправили — "в текущей реализации, которая может измениться"...
Подождите ...
Wait...
Пока на собственное сообщение не было ответов, его можно удалить.