Pull to refresh

Comments 19

PinnedPinned comments

Корень проблемы .ConfigureAwait(...) на мой взгляд в том, что она сталкивает разработчиков библиотек и UI-кода:

  • разработчики библиотек не хотят засорять код .ConfigureAwait(false)

  • разработчики UI — .ConfigureAwait(true), await Task.Run(() => ...)

Наиболее автоматизированное решение сейчас — генерировать .ConfigureAwait(false) через Fody. Но Fody — сторонняя приблуда, ещё и усложняющая сборку проекта. Конечно хочется нативного решения, на уровне dotnet sdk или рантайма, и идей, как это сделать описано уже много.

Атрибут для сборки/параметр *.csproj
https://github.com/dotnet/csharplang/issues/2542

Переопредение оператора await на уровне проекта
https://github.com/dotnet/csharplang/issues/2649

Короткий синтаксис для `.ConfigureAwait(false)
https://github.com/dotnet/csharplang/discussions/645

Новый вид Task/Task<T>, свободный от контекста (даже есть реализация, но кажется, что overkill)
https://github.com/ufcpp/ContextFreeTask

Если добавить в язык условный await(false) или await!! грязи меньше не станет, поэтому остановимся на первом proposal. Его можно реализовать, например, в виде configureawait context, по аналогии с nullable context

С ним в *.csproj можно было бы писать:

<ConfigureAwait>false</ConfigureAwait>

А в файле с кодом *.cs:

#configureawait true
public static async Task ContextDependentMethod() { ... }
#configureawait restore

Только увы, proposal висит с 2015 года, и никакого результата. Cейчас .ConfigureAwait(false) реализован в виде структуры-обёртки над Task, т.е. на уровне стандартной библиотеки, реализация же proposal потребует знания о ConfigureAwait на стороне компилятора (Roslyn) или рантайма. Возможно, сложность заключается в этом. Или просто всем без разницы и время на это не выделили.

Есть вопросы, возможно глупые...

  1. Решить проблему на стороне вызывающего кода.

Чем Task.Run лучше ConfigureAwait(false)? Можно ли var text = await Task.Run(() => GetTextAsync()); // внутри этой лямбды SynchronizationContext.Current == null заменить на var text = await GetTextAsync().ConfigureAwait(false);. Что правильнее, быстрее, лучше и почему? В доках вроде как написано использовать второе.

  1. Использовать правильное синхронное ожидание

Тот же вопрос, почему используется Task.Run, а не ConfigureAwait(false)? Чем это лучше и правильнее?

  1. Однократный переход в Thread Pool

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

Можно ли var text = await Task.Run(() => GetTextAsync()); заменить на var text = await GetTextAsync().ConfigureAwait(false);
Тот же вопрос, почему используется Task.Run, а не ConfigureAwait(false)

.ConfigureAwait(false) имеет смысл только в методах, которым не нужно возвращаться на UI поток. Если использовать его внутри метода, которому важен контекст, то произойдёт исключение при попытке обновить UI (Text.Text = text;), если выполнение перейдёт на Thread Pool.

Метод Task.Run в этих примерах разделяет код, которому контекст важен, и код, который может работать на Thread Pool без возврата в исходный контекст. Весь код, запущенный через Task.Run ничего не узнает про контекст синхронизации, в то же время Task, возвращённый Task.Run может ожидаться в нужном контексте — хоть синхронно, хоть асинхронно.

Спасибо за ответ, я понял в чем разница.

По итогу выходит лучшее использовать первый способ если у вас асинхронный процесс редко отдает управление (может надолго заморозить UI поток). Например можно добавить Thread.Delay(1000) в метод GetTextAsync() для моделирования такого случая.

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

Остальные советы выглядят для меня больше как костыли... Или нужны в сложных случаях которые мне пока непонятны...

Кстати для таких сложных случаев можно ли использовать https://github.com/microsoft/vs-threading которую вы привели как пример или эта библиотека потянет много ненужных зависимостей? По названию она не выглядит как универсальная...

Остальные советы выглядят для меня больше как костыли
Ну почему костыли.

Если вы уже согласились на Task.Run, пункт 2) позволит не делать лишнее переключение потока и обратно, если мы уже на ThreadPool, вызывая Task.Run только в нужных случаях.

Пункт 3) приятный синтаксический сахар.
Единожды написать в начале метода await TaskEx.EscapeContext(); и дальше можно писать await-ы подряд, не задумываясь о ConfigureAwait на каждом await
var result1 = await RunQuery1(param);
var result2 = await RunQuery2(result1);
var result3 = await RunQuery3(result2);

Корень проблемы .ConfigureAwait(...) на мой взгляд в том, что она сталкивает разработчиков библиотек и UI-кода:

  • разработчики библиотек не хотят засорять код .ConfigureAwait(false)

  • разработчики UI — .ConfigureAwait(true), await Task.Run(() => ...)

Наиболее автоматизированное решение сейчас — генерировать .ConfigureAwait(false) через Fody. Но Fody — сторонняя приблуда, ещё и усложняющая сборку проекта. Конечно хочется нативного решения, на уровне dotnet sdk или рантайма, и идей, как это сделать описано уже много.

Атрибут для сборки/параметр *.csproj
https://github.com/dotnet/csharplang/issues/2542

Переопредение оператора await на уровне проекта
https://github.com/dotnet/csharplang/issues/2649

Короткий синтаксис для `.ConfigureAwait(false)
https://github.com/dotnet/csharplang/discussions/645

Новый вид Task/Task<T>, свободный от контекста (даже есть реализация, но кажется, что overkill)
https://github.com/ufcpp/ContextFreeTask

Если добавить в язык условный await(false) или await!! грязи меньше не станет, поэтому остановимся на первом proposal. Его можно реализовать, например, в виде configureawait context, по аналогии с nullable context

С ним в *.csproj можно было бы писать:

<ConfigureAwait>false</ConfigureAwait>

А в файле с кодом *.cs:

#configureawait true
public static async Task ContextDependentMethod() { ... }
#configureawait restore

Только увы, proposal висит с 2015 года, и никакого результата. Cейчас .ConfigureAwait(false) реализован в виде структуры-обёртки над Task, т.е. на уровне стандартной библиотеки, реализация же proposal потребует знания о ConfigureAwait на стороне компилятора (Roslyn) или рантайма. Возможно, сложность заключается в этом. Или просто всем без разницы и время на это не выделили.

<тут было было про ненужноепро ConfigureAwait(true);? Удалено>

А по поводу, что с этим делать, я сторонник того, чтобы указывать компилятору, как именно надо разворачивать await в async-методе: либо подразумевать, что await оставляет код в том же контекте синхронизации (для разработчиков приложений), как это сделано сейчас, либо запускает продолжение выполнения метода в ThreadPool (это для разрабочиков библиотек и всяческих вспомогательных методов, которым котнтекст синхронизации фреймворка для работы не нужен).
Лучший IMHO способ укзать это - использовать атрибут, т.к. его можно указывать и для метода, и для класса, и для всей сборки.

Поаккуратнее с примерами. Вот вы приводите пример:

async Task<string> GetTextAsync()
{
await TaskEx.EscapeContext(); // await TaskScheduler.Default;
var request = CreateRequest(authToken);
var response = await client.SendAsync(request);
// .ConfigureAwait больше не нужен //
GetTextAsync continuation var text = Deserialize(response);
return text;
}

Способ подсмотрен в dotnet/runtime. Также есть issue о добавлении публичного API и готовая реализация в Microsoft.VisualStudio.Threading.

А что такое TaskEx.EscapeContext()? В стандартной библиотеке времени выполнения такого класса и метода нет, в классе Task такого метода нет тоже. И по ссылкам в GitHub в текущей версии его тоже нет.
Конечно, если прочитать issue по вашей сcылке да порыться в сети, то можно выяснить, откуда у этого кода ноги растут - но это явно не для того уровня читателей, на который (полагаю) рассчитана эта статья.

PS А вообще-то, на Хабре некогда уже был опубликован перевод статьи Тэлбота из блога разработчиков MS, в которой хорошо, без лишних сложностей - и, главное, из первых рук - объяснено, что это за зверь такой - ConfigureAwait(false). Что IMHO несколько снижает ценность этой статьи

PPS Отдельный упрек редактору в новой версии Хабра, который не позволяет нормально процитировать блок кода.

Так двумя абзацами ниже реализация же приведена. Хотя соглашусь, лучше бы комментарием отметить, что «реализацию смотрите ниже» или даже сразу её в тот же блок кода засунуть, а то действительно поначалу сбивает с толку.

Ну да, увидел, в конце концов - в конце длинного блока кода, про связь которого с предыдущем блоком в статье явно не упомянуто. А еще лично меня сбило с толку, что когда-то (лет десять назад в какой-то промежуточной версии .NET Framework) этот класс и этот метод таки были (но в окончательную версию не вошли), и следы этого остались и в памяти, и в интернете, во всяких разных примерах.

А в чем проблема делать это на стороне библиотечного кода? Библиотека на то и библиотека что у нее нет информации о контексте и о том как ее используют. Например в котлине считается что библиотечную корутину должно быть можно запускать на любом треде/диспетчере, а она сама уже разберётся что ей нужно:

suspend fun loadAndParseJson(): Json {
  val bytes = withContext(Dispatchers.IO) {
    // IO is for blocking I/O
    // Reading from file
  }
  val json = withContext(Dispatchers.Default) {
    // Default is for computations
    // Parsing data
  }
  return json
}

В данном примере форсируется переключение потоков, а легко можно придумать пример приложения, которому чтение json и его парсинг допустимо делать в одном потоке, без переключения. В общем случае неважно, но для перфекционистов может быть эстетически неприемлемо.

По чему не сделать чтобы библиотечный метод просто возвращал Task.Run, а там уже пусть как хотят, так и ждут?

Task<string> GetTextAsync()
{
    return Task.Run(async () =>
    {
        var request  = CreateRequest(authToken);
        var response = await client.SendAsync(request);

        var text = Deserialize(response);
        return text;
    });
}

Такой способ будет работать, но дополнительный Task будет создаваться даже если в вызывающем коде нет контекста.
Можно доработать и перекладывать в Thread Pool только если контекст есть, получится эквивалент способа 3 (с await EscapeContext())

Для I/O-bound задач Task.Run плох тем, что блокирует поток из ThreadPool'а до момента завершения задачи. А одно из достоинств модели async/await именно в масштабируемости, т.е. в экономии на потоках.

Блокировка UI-потока гораздо заметнее блокировки фонового потока, поэтому Task.Run(() => ...) может быть меньшим злом.

Если запускаемая функция блокирует поток Thread Pool и это ухудшает производительность — её можно запустить не на Thread Pool, а в отдельном потоке, создав его вручную, или следующим образом (в нынешней реализации .NET этот способ тоже создаёт новый поток):

Task.Factory.StartNew(
  () => { ... },
  TaskCreationOptions.LongRunning|TaskCreationOptions.HideScheduler);

В этом случае, блокирующий код внутри метода будет выполняться в отдельном потоке и заблокирует только его.

Однако постоянное создание новых потоков может ещё больше навредить производительности, а при выносе вычислений в отдельные потоки пропускная способность Thread Pool также может снизиться, но уже из-за загруженности самих процессорных ядер. Поэтому не всегда целесообразно использование новых потоков или флага LongRunning.

Согласен, эта проблема актуальна больше для сервера. Но вот тоже интересный подход применительно к UI - прокачивать сообщения в момент простоя.

Если уж заморачиваться такими вещами, то правильно не использовать блокирующие файловые операции, например
File.ReadAllBytes();
заменить на
await File.ReadAllBytesAsync();

Sign up to leave a comment.

Articles