Трябва ли да се използва ConfigureAwait(falseв библиотеки, които извикват асинхронни callback?

+5 гласа
125 прегледа
попитан 2016 юли 6 от Nikoleta.V. (4,090 точки)

Има доста насоки кога да се използва ConfigureAwait(false) при ползване на async/await в C#. 

Основната препоръка е да се използва ConfigureAwait(false) в библиотеки,тъй като рядко зависи от synchronization context-а. 

Обаче пишем много generic код, който приема функция като вход. Прост пример може да бъде следния код на комбинатори: 

Map: 

public static async Task<TResult> Map<T, TResult>(this Task<T> task, Func<T, TResult> mapping) 

    return mapping(await task); 

FlatMap: 

public static async Task<TResult> FlatMap<T, TResult>(this Task<T> task, Func<T, Task<TResult>> mapping) 

    return await mapping(await task); 

Питането ми е дали трябва да се използва ConfigureAwait(false)  в този случай? Не съм сигурен как context capture работи. 

От една страна, ако комбинаторите се ползват по функционален начин, synchronization context ще е излишен. От друга страна, хората може да използват неправилно апито и да правят неща,зависими от контекста. 

Единият вариант е да има отделни методи за всеки случай (Map или MapWithContextCapture), ама изглежда грозно. 

Друг вариант е да се добави опция map/flatmap от и в ConfiguredTaskAwaitable<T>,но тъй като нещата, които се await-ват не трябва да имплементират интерфейс това би се оказано доста излишен код и според мен е още по-зле. 

Проблемът е следния- когато се изпълни колбек във функцията, добавянето на ConfigureAwait(false) ще доведе до null synchronization context. Имах идея да добавя булев флаф в метода, но не е много красиво,а и ще се разнася из цялото апи, тъй като доста функции зависят от тези горе. 

Има ли добър начин да сменя функционалността на извикващия метод, така че имплементираната библиотека да не се интересува дали се използва контекста в подадените функции?  

Как да се справя с тази ситуация? Да игнорирам ли факта, че някой може да използва sync контекста или има по-добър начин да сменя отговорността към извикващия метод, без да правя overload, булев флаг или нещо подобно 

1 отговор

0 гласа
отговорени 2016 юли 13 от valeri.hristov (7,340 точки)

Като направиш await task.ConfigureAwait(false) се пренасяш към threadpool-а, карайки mapping да върви в null context. Това може да доведе до различно поведение:

await Map(0, i => { myTextBox.Text = i.ToString(); return 0; });

Това ще се счупи при следната имплементация на Map:

var result = await task.ConfigureAwait(false);

return await mapper(result);

Но не и тук:

var result = await task/*.ConfigureAwait(false)*/;

И още по-гадно:

var result = await task.ConfigureAwait(new Random().Next() % 2 == 0);

Ези и тура със synchronization context-а! Изглежда забавно,ама не. По-реалистичен пример:

var result =

  someConfigFlag ? await GetSomeValue<T>() :

  await task.ConfigureAwait(false);

В зависимост от някакво външно състояние, останалата част от метода може да върви под различен synchronization context.

Това е слабост в дизайна на await.

Най-дразнещия проблем тук е, че при извикването на апито не е ясно какво ще се случи. Това е объркващо и води до бъгове. Добре е да си подсигуриш предвидимо държание като винаги използваш task.ConfigureAwait(false).

Ламбда израза трябва да се уверява, че върви в правилния контекст:

var uiScheduler = TaskScheduler.FromCurrentSynchronizationContext;

Map(..., async x => await Task.Factory.StartNew(

        () => { /*access UI*/ },

        CancellationToken.None, TaskCreationOptions.None, uiScheduler));

Не е елегантно решение, но мисля че е най-малкото зло от всичките възможности.

...