Повторная задача Task.ConfigureAwait(continueOnCapturedContext: false)
Слишком долго для чтения. Использование 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: если он используется правильно, он снова уменьшает переключение потоков на стороне сервера.
Ответы
Ответ 1
Когда вы имеете дело с асинхронными операциями, накладные расходы на коммутатор потоков слишком малы, чтобы заботиться (вообще говоря). Цель ConfigureAwait(false)
заключается не в том, чтобы вызвать переключатель потока (если необходимо), а скорее для предотвращения слишком большого количества кода, выполняемого в определенном специальном контексте.
Обоснованием этого шаблона было то, что "он помогает избежать взаимоблокировок".
И погружения в стек.
Но я думаю, что это не проблема в общем случае. Когда я сталкиваюсь с кодом, который неправильно использует ConfigureAwait
, я просто переношу его в Task.Run
и двигаюсь дальше. Накладные расходы на коммутаторы потоков не стоит беспокоиться.
Ответ 2
Основная цель проекта ConfigureAwait (false) заключается в уменьшении избыточных обратных вызовов продолжения SynchronizationContext.Post для ожидания, где это возможно. Обычно это означает меньшее переключение потоков и меньшую работу с потоками пользовательского интерфейса.
Я не согласен с вашей предпосылкой. Цель ConfigureAwait(false)
состоит в том, чтобы сократить, насколько это возможно, работу, которая должна быть перенаправлена на "специальные" (например, пользовательский интерфейс) контексты несмотря на потоков, которые могут потребоваться от этого контекста,
Если целью было сократить поточные коммутаторы, вы могли бы просто оставаться в одном и том же специальном контексте во всей работе, а затем не требовать других потоков.
Чтобы достичь этого, вы должны использовать ConfigureAwait
везде, вам не нужен поток, выполняющий продолжение. Если вы возьмете свой пример и используете ConfigureAwait
соответственно, вы получите только один переключатель (вместо 2 без него):
private async void Button_Click(object sender, RoutedEventArgs e)
{
TaskExt.Log("A1");
await AnotherClass.MethodAsync().ConfigureAwait(false);
TaskExt.Log("A2");
}
public class AnotherClass
{
public static async Task MethodAsync()
{
TaskExt.Log("B1");
await SomeClass.SomeAsyncApi().ConfigureAwait(false);
TaskExt.Log("B2");
}
}
public class SomeClass
{
public static async Task<int> SomeAsyncApi()
{
TaskExt.Log("X1");
await Task.Delay(1000).WithCompletionLog(step: "X1.5").ConfigureAwait(false);
TaskExt.Log("X2");
return 42;
}
}
Вывод:
{ step = A1, thread = 9 }
{ step = B1, thread = 9 }
{ step = X1, thread = 9 }
{ step = X1.5, thread = 11 }
{ step = X2, thread = 11 }
{ step = B2, thread = 11 }
{ step = A2, thread = 11 }
Теперь, когда вы заботитесь о продолжении потока (например, когда вы используете элементы управления пользовательским интерфейсом), вы "платите", переключаясь на этот поток, размещая соответствующую работу в этом потоке. Вы все еще получили от всей работы, которая не требовала этого потока.
Если вы хотите сделать это еще глубже и удалить синхронную работу этих методов async
из потока пользовательского интерфейса, вам нужно только один раз использовать Task.Run
и добавить еще один переключатель:
private async void Button_Click(object sender, RoutedEventArgs e)
{
TaskExt.Log("A1");
await Task.Run(() => AnotherClass.MethodAsync()).ConfigureAwait(false);
TaskExt.Log("A2");
}
Вывод:
{ step = A1, thread = 9 }
{ step = B1, thread = 10 }
{ step = X1, thread = 10 }
{ step = X1.5, thread = 11 }
{ step = X2, thread = 11 }
{ step = B2, thread = 11 }
{ step = A2, thread = 11 }
Это руководство по использованию ConfigureAwait(false)
направлено на разработчиков библиотек, потому что там, где это действительно важно, но нужно использовать его всякий раз, когда вы можете, и в этом случае вы уменьшаете работу в этих специальных контекстах, сохраняя при этом переключение потоков на минимум.
Использование WithNoContext
имеет точно такой же результат, как и при использовании ConfigureAwait(false)
. Однако недостатки связаны с потоком SynchronizationContext
и что вы не знаете об этом в методе async
. ConfigureAwait
напрямую влияет на текущий await
, так что у вас есть причина и следствие вместе.
Использование Task.Run
тоже, как я уже указывал, имеет точно такой же результат использования ConfigureAwait(false)
везде с добавленной стоимостью разгрузки синхронных частей метода async
до ThreadPool
. Если это необходимо, то Task.Run
подходит, в противном случае ConfigureAwait(false)
достаточно.
Теперь, если вы имеете дело с багги-библиотекой, когда ConfigureAwait(false)
не используется должным образом, вы можете взломать ее, удалив SynchronizationContext
, но используя Thread.Run
, намного проще и понятнее и разгрузите работу ThreadPool
имеет очень незначительные накладные расходы.