Почему это асинхронное действие зависает?
У меня есть многоуровневое приложение .Net 4.5, вызывающее метод с использованием нового async
C # и await
ключевых слов, которые просто зависают, и я не понимаю, почему.
Внизу у меня есть метод async, который расширяет нашу утилиту базы данных OurDBConn
(в основном оболочку для базовых DBConnection
и DBCommand
):
public static async Task ExecuteAsync(this OurDBConn dataSource, Func function) { string connectionString = dataSource.ConnectionString; // Start the SQL and pass back to the caller until finished T result = await Task.Run( () => { // Copy the SQL connection so that we don't get two commands running at the same time on the same open connection using (var ds = new OurDBConn(connectionString)) { return function(ds); } }); return result; }
Затем у меня есть asynchronous метод среднего уровня, который вызывает это, чтобы получить несколько медленных текущих итогов:
- Выполнение задачи в фоновом режиме в приложении WPF
- В чем разница между возвратом пустоты и возвратом задачи?
- Запуск двух асинхронных задач параллельно и сбор результатов в .NET 4.5
- Запуск задач в foreach Loop использует значение последнего элемента
- Очередь процесса с многопоточным или задачами
public static async Task GetTotalAsync( ... ) { var result = await this.DBConnection.ExecuteAsync( ds => ds.Execute("select slow running data into result")); return result; }
Наконец, у меня есть метод UI (действие MVC), которое выполняется синхронно:
Task asyncTask = midLevelClass.GetTotalAsync(...); // do other stuff that takes a few seconds ResultClass slowTotal = asyncTask.Result;
Проблема в том, что она вечно лежит на этой последней строке. Он делает то же самое, если я вызываю asyncTask.Wait()
. Если я запускаю медленный метод SQL напрямую, это занимает около 4 секунд.
Поведение, которое я ожидаю, заключается в том, что когда он добирается до asyncTask.Result
, если он еще не закончен, он должен подождать, пока он не появится, и как только он вернется, он должен вернуть результат.
Если я перейду к отладчику, то инструкция SQL завершится, и функция lambda завершится, но return result;
линия GetTotalAsync
никогда не достигается.
Любая идея, что я делаю неправильно?
Любые предложения о том, где мне нужно исследовать, чтобы исправить это?
Может ли это быть тупиком где-то, и если да, то есть какой-то прямой способ его найти?
- Шаблон для самоотдачи и перезапуска задачи
- Когда использовать Task.Delay, когда использовать Thread.Sleep?
- Секвенирование и реорганизация задач
- Использовать Task.Run () в синхронном методе, чтобы избежать тупиковой остановки в асинхронном методе?
- Разница между TPL и async / await (Обработка streamов)
- Выполнение задач параллельно
- WaitAll vs WhenAll
- Лучший способ конвертировать метод async на основе обратного вызова в ожидаемую задачу
Да, это тупик. И распространенная ошибка с TPL, так что не чувствуйте себя плохо.
Когда вы пишете, await foo
, среда выполнения, по умолчанию, планирует продолжение функции в том же SynchronizationContext, что метод начал. На английском языке, допустим, вы вызвали ExecuteAsync
из streamа пользовательского интерфейса. Ваш запрос выполняется в streamе threadpool (потому что вы вызвали Task.Run
), но вы Task.Run
результата. Это означает, что среда выполнения будет планировать вашу линию « return result;
», чтобы вернуться к streamу пользовательского интерфейса, а не планировать ее обратно в threadpool.
Так как же этот тупик? Представьте, что у вас есть этот код:
var task = dataSource.ExecuteAsync(_ => 42); var result = task.Result;
Итак, первая строка запускает асинхронную работу. Вторая строка блокирует stream пользовательского интерфейса . Поэтому, когда среда выполнения хочет запустить строку «return result» обратно в streamе пользовательского интерфейса, она не сможет этого сделать до тех пор, пока Result
завершится. Но, конечно, результат не может быть дан до тех пор, пока не произойдет возrotation. Тупик.
Это иллюстрирует ключевое правило использования TPL: когда вы используете .Result
в streamе пользовательского интерфейса (или какой-либо другой причудливый контекст синхронизации), вы должны быть осторожны, чтобы гарантировать, что ничто, что зависит от задачи, не запланировано для streamа пользовательского интерфейса. Или происходит зло.
Ну так что ты делаешь? Вариант №1 используется везде, но, как вы сказали, это уже не вариант. Второй вариант, который вам доступен, – это просто прекратить использовать ожидание. Вы можете переписать две функции:
public static Task ExecuteAsync (this OurDBConn dataSource, Func function) { string connectionString = dataSource.ConnectionString; // Start the SQL and pass back to the caller until finished return Task.Run( () => { // Copy the SQL connection so that we don't get two commands running at the same time on the same open connection using (var ds = new OurDBConn(connectionString)) { return function(ds); } }); } public static Task GetTotalAsync( ... ) { return this.DBConnection.ExecuteAsync ( ds => ds.Execute("select slow running data into result")); }
Какая разница? В настоящее время нет нигде, поэтому ничто не подразумевается для streamа пользовательского интерфейса. Для простых методов, подобных этим, которые имеют один возврат, нет смысла делать шаблон « var result = await...; return result
»; просто удалите модификатор async и передайте объект задачи непосредственно. Это меньше накладных расходов, если ничего другого.
Вариант № 3 заключается в том, чтобы указать, что вы не хотите, чтобы ваши ожидания планировали вернуться к streamу пользовательского интерфейса, а просто планировали stream пользовательского интерфейса. Вы делаете это с помощью метода ConfigureAwait
, например:
public static async Task GetTotalAsync( ... ) { var resultTask = this.DBConnection.ExecuteAsync ( ds => return ds.Execute("select slow running data into result"); return await resultTask.ConfigureAwait(false); }
В ожидании задачи, как правило, планировалось использовать stream пользовательского интерфейса, если вы на нем; ожидая результата ContinueAwait
будет игнорировать любой контекст, в котором вы находитесь, и всегда планируете использовать threadpool. Недостатком этого является то, что вы должны посыпать это повсюду во всех функциях, на которые зависит ваш. Результат зависит, потому что любая пропущенная .ConfigureAwait
может быть причиной другого тупика.
Это classический сценарий смешанного async
тупика, о котором я рассказываю в своем блоге . Джейсон описал это хорошо: по умолчанию «контекст» сохраняется при каждом await
и используется для продолжения async
метода. Этот «контекст» представляет собой текущий SynchronizationContext
если он не равен null
, и в этом случае это текущий TaskScheduler
. Когда метод async
пытается продолжить, он сначала повторно вводит захваченный «контекст» (в данном случае, ASP.NET SynchronizationContext
). ASP.NET SynchronizationContext
разрешает только один stream в контексте за один раз, и в контексте уже есть stream – stream, заблокированный в Task.Result
.
Существует два руководства, которые позволят избежать этого тупика:
- Используйте
async
полностью вниз. Вы упоминаете, что вы «не можете» это сделать, но я не уверен, почему нет. ASP.NET MVC на .NET 4.5, безусловно, может поддерживатьasync
действия, и это не сложно сделать. - Используйте
ConfigureAwait(continueOnCapturedContext: false)
как можно больше. Это отменяет поведение по умолчанию возобновления в захваченном контексте.
Я был в тупиковой ситуации, но в моем случае, вызвав метод async из метода синхронизации, все, что работает для меня, было:
private static SiteMetadataCacheItem GetCachedItem() { TenantService TS = new TenantService(); // my service datacontext var CachedItem = Task.Run(async ()=> await TS.GetTenantDataAsync(TenantIdValue) ).Result; // dont deadlock anymore }
это хороший подход, любая идея?
Просто чтобы добавить к принятому ответу (недостаточно комментариев для комментариев), у меня возникла эта проблема при блокировке использования task.Result
, событие, хотя каждый из них task.Result
ниже, имел ConfigureAwait(false)
, как в этом примере:
public Foo GetFooSynchronous() { var foo = new Foo(); foo.Info = GetInfoAsync.Result; // often deadlocks in ASP.NET return foo; } private async Task GetInfoAsync() { return await ExternalLibraryStringAsync().ConfigureAwait(false); }
Проблема действительно связана с кодом внешней библиотеки. Метод асинхронной библиотеки попытался продолжить в контексте синхронизации вызовов, независимо от того, как я настроил ожидание, что привело к взаимоблокировке.
Таким образом, ответ состоял в том, чтобы свернуть мою собственную версию внешнего библиотечного кода ExternalLibraryStringAsync
, чтобы он имел желаемые свойства продолжения.
неправильный ответ для исторических целей
После сильной боли и муки я нашел решение, захороненное в этом сообщении в блоге (Ctrl-f для «тупика»). Он вращается вокруг с помощью task.ContinueWith
, вместо голого task.Result
.
Предыдущий пример блокировки:
public Foo GetFooSynchronous() { var foo = new Foo(); foo.Info = GetInfoAsync.Result; // often deadlocks in ASP.NET return foo; } private async Task GetInfoAsync() { return await ExternalLibraryStringAsync().ConfigureAwait(false); }
Избегайте тупика следующим образом:
public Foo GetFooSynchronous { var foo = new Foo(); GetInfoAsync() // ContinueWith doesn't run until the task is complete .ContinueWith(task => foo.Info = task.Result); return foo; } private async Task GetInfoAsync { return await ExternalLibraryStringAsync().ConfigureAwait(false); }