Слишком долго для чтения. Использование Task.ConfigureAwait(continueOnCapturedContext: false)
может привести к избыточному переключению потоков. Я ищу для вас последовательное решение.
Длинная версия. Основная цель проекта ConfigureAwait(false)
заключается в уменьшении избыточных обратных вызовов продолжения SynchronizationContext.Post
для await
, где это возможно. Обычно это означает меньшее переключение потоков и меньшую работу с потоками пользовательского интерфейса. Однако это не всегда так, как это работает.
Например, существует сторонняя библиотека, реализующая API SomeAsyncApi
. Обратите внимание, что ConfigureAwait(false)
не используется нигде в этой библиотеке по какой-либо причине:
// some library, SomeClass class
public static async Task<int> SomeAsyncApi()
{
TaskExt.Log("X1");
// await Task.Delay(1000) without ConfigureAwait(false);
// WithCompletionLog only shows the actual Task.Delay completion thread
// and doesn't change the awaiter behavior
await Task.Delay(1000).WithCompletionLog(step: "X1.5");
TaskExt.Log("X2");
return 42;
}
// logging helpers
public static partial class TaskExt
{
public static void Log(string step)
{
Debug.WriteLine(new { step, thread = Environment.CurrentManagedThreadId });
}
public static Task WithCompletionLog(this Task anteTask, string step)
{
return anteTask.ContinueWith(
_ => Log(step),
CancellationToken.None,
TaskContinuationOptions.ExecuteSynchronously,
TaskScheduler.Default);
}
}
Теперь, скажем, есть какой-то клиентский код, запущенный в потоке UI WinForms, и используя SomeAsyncApi
:
// another library, AnotherClass class
public static async Task MethodAsync()
{
TaskExt.Log("B1");
await SomeClass.SomeAsyncApi().ConfigureAwait(false);
TaskExt.Log("B2");
}
// ...
// a WinFroms app
private async void Form1_Load(object sender, EventArgs e)
{
TaskExt.Log("A1");
await AnotherClass.MethodAsync();
TaskExt.Log("A2");
}
Выход:
{ step = A1, thread = 9 } { step = B1, thread = 9 } { step = X1, thread = 9 } { step = X1.5, thread = 11 } { step = X2, thread = 9 } { step = B2, thread = 11 } { step = A2, thread = 9 }
Здесь логический поток выполнения проходит через 4 потока. 2 из них являются избыточными и вызваны SomeAsyncApi().ConfigureAwait(false)
. Это происходит потому, что ConfigureAwait(false)
переносит продолжение в ThreadPool
из потока с контекстом синхронизации (в этом случае поток пользовательского интерфейса).
В этом конкретном случае MethodAsync
лучше без ConfigureAwait(false)
. Тогда он принимает только 2 переключателя нити vs 4:
{ step = A1, thread = 9 } { step = B1, thread = 9 } { step = X1, thread = 9 } { step = X1.5, thread = 11 } { step = X2, thread = 9 } { step = B2, thread = 9 } { step = A2, thread = 9 }
Однако автор MethodAsync
использует ConfigureAwait(false)
со всеми благими намерениями и после лучшие практики, и она ничего не знает о внутренняя реализация SomeAsyncApi
. Не было бы проблем, если бы ConfigureAwait(false)
использовался "весь путь" (т.е. внутри SomeAsyncApi
тоже), но это не подходило ей под контроль.
Как это происходит с WindowsFormsSynchronizationContext
(или DispatcherSynchronizationContext
), где мы, возможно, не заботимся о дополнительных переключателях потоков вообще. Однако аналогичная ситуация может возникнуть в ASP.NET, где AspNetSynchronizationContext.Post
по существу делает следующее:
Task newTask = _lastScheduledTask.ContinueWith(_ => SafeWrapCallback(action));
_lastScheduledTask = newTask;
Все это может выглядеть как надуманная проблема, но я видел много такого производственного кода, как на стороне клиента, так и на стороне сервера. Еще один сомнительный шаблон, с которым я столкнулся: await TaskCompletionSource.Task.ConfigureAwait(false)
с SetResult
вызывается в том же контексте синхронизации, что и для первого await
. Опять же, продолжение было избыточно перенесено на ThreadPool
. Причиной этого шаблона было то, что "он помогает избежать взаимоблокировок".
Вопрос. В свете описанного поведения ConfigureAwait(false)
я ищу элегантный способ использования async/await
, при этом минимизируя избыточное переключение потоков/контекстов. В идеале, что-то, что будет работать с существующими сторонними библиотеками.
На что я смотрел, пока:
-
Разгрузка
async
лямбда сTask.Run
не идеальна, поскольку она вводит по крайней мере один дополнительный переключатель потока (хотя это может потенциально спасти многие другие):await Task.Run(() => SomeAsyncApi()).ConfigureAwait(false);
-
Еще одно хакерское решение может заключаться в том, чтобы временно удалить контекст синхронизации из текущего потока, поэтому он не будет захвачен никакими последующими ожиданиями во внутренней цепочке вызовов (ранее я упоминал его здесь):
async Task MethodAsync() { TaskExt.Log("B1"); await TaskExt.WithNoContext(() => SomeAsyncApi()).ConfigureAwait(false); TaskExt.Log("B2"); }
{ step = A1, thread = 8 } { step = B1, thread = 8 } { step = X1, thread = 8 } { step = X1.5, thread = 10 } { step = X2, thread = 10 } { step = B2, thread = 10 } { step = A2, thread = 8 }
public static Task<TResult> WithNoContext<TResult>(Func<Task<TResult>> func) { Task<TResult> task; var sc = SynchronizationContext.Current; try { SynchronizationContext.SetSynchronizationContext(null); // do not await the task here, so the SC is restored right after // the execution point hits the first await inside func task = func(); } finally { SynchronizationContext.SetSynchronizationContext(sc); } return task; }
Это работает, но мне не нравится тот факт, что он меняет текущий контекст синхронизации потока, хотя и очень короткий. Более того, здесь существует еще одна импликация: при отсутствии
SynchronizationContext
в текущем потоке дляawait
продолжения будет использоваться окружающая средаTaskScheduler.Current
. Чтобы объяснить это,WithNoContext
можно было бы изменить, как показано ниже, что сделало бы этот взлом еще более экзотическим:// task = func(); var task2 = new Task<Task<TResult>>(() => func()); task2.RunSynchronously(TaskScheduler.Default); task = task2.Unwrap();
Буду признателен за любые другие идеи.
Обновлено, чтобы отправить @комментарий i3arnon:
Я бы сказал, что это наоборот, потому что, как сказал Стивен в его ответ "Цель ConfigureAwait (false) заключается не в том, чтобы вызвать (если необходимо), а скорее, чтобы предотвратить слишком много кода работающий в определенном специальном контексте", с которым вы не согласны и является корнем вашего совместимого.
Как ваш ответ был отредактирован, вот ваше выражение Я не согласился, для ясности:
ConfigureAwait (false) Цель состоит в том, чтобы уменьшить, насколько это возможно, работу "специальные" (например, UI) потоки должны обрабатываться, несмотря на поток для переключения требуется.
Я также не согласен с вашей текущей версией этого заявления. Я приведу вас к первому источнику, Стивен Тууб сообщение в блоге:
Избегайте ненужного маршалинга
Если это вообще возможно, убедитесь, что вы выполняете асинхронную реализацию не нуждается в заблокированном потоке, чтобы завершить операцию (таким образом, вы можете просто использовать обычные блокирующие механизмы для ожидания синхронно для выполнения асинхронной работы в другом месте). в случае async/await, это обычно означает, что любой ожидает внутри асинхронной реализации, вы используете ConfigureAwait (false) для всех ожидающих точек; это предотвратит ожидание от попытки вернуться к текущему SynchronizationContext. В виде разработчик библиотеки, его наилучшая практика всегда использовать ConfigureAwait (false) на всех ваших ожиданиях, если у вас нет конкретная причина не; это хорошо не только для того, чтобы виды проблем с тупиками, , но также и для производительности, поскольку это позволяет избежать ненужные затраты на маршалинг.
В нем говорится, что цель состоит в том, чтобы избежать ненужных затрат на маршалинг, для производительности. Переключатель потока (который, в частности, течет ExecutionContext
), является большой стоимостью маршалинга.
Теперь он нигде не говорит о том, что цель состоит в том, чтобы уменьшить объем работы, который выполняется на "специальных" потоках или контекстах.
Хотя это может иметь определенный смысл для потоков пользовательского интерфейса, я все еще не думаю, что это главная цель, стоящая за ConfigureAwait
. Существуют и другие - более структурированные способы минимизации работы с потоками пользовательского интерфейса, например, с использованием фрагментов await Task.Run(work)
.
Кроме того, совершенно не имеет смысла сводить к минимуму работу над AspNetSynchronizationContext
- которая сама течет из потока в поток, в отличие от потока пользовательского интерфейса. Совсем наоборот, как только вы на AspNetSynchronizationContext
, вы хотите сделать как можно больше работы, чтобы избежать ненужного переключения в середине обработки HTTP-запроса. Тем не менее, все еще имеет смысл использовать ConfigureAwait(false)
в ASP.NET: если он используется правильно, он снова уменьшает переключение потоков на стороне сервера.