Почему зависает это асинхронное действие?

103

У меня есть многоуровневое приложение .Net 4.5, вызывающее метод с использованием ключевых слов C # new asyncи, awaitкоторые просто зависают, и я не понимаю, почему.

Внизу у меня есть асинхронный метод, который расширяет нашу утилиту базы данных OurDBConn(в основном оболочка для базовых объектов DBConnectionи DBCommandобъектов):

public static async Task<T> ExecuteAsync<T>(this OurDBConn dataSource, Func<OurDBConn, T> 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;
}

Затем у меня есть асинхронный метод среднего уровня, который вызывает это, чтобы получить некоторые медленные итоги:

public static async Task<ResultClass> GetTotalAsync( ... )
{
    var result = await this.DBConnection.ExecuteAsync<ResultClass>(
        ds => ds.Execute("select slow running data into result"));

    return result;
}

Наконец, у меня есть метод пользовательского интерфейса (действие MVC), который выполняется синхронно:

Task<ResultClass> asyncTask = midLevelClass.GetTotalAsync(...);

// do other stuff that takes a few seconds

ResultClass slowTotal = asyncTask.Result;

Проблема в том, что он навсегда висит на последней строке. То же самое происходит, если я звоню asyncTask.Wait(). Если я запустил медленный метод SQL напрямую, это займет около 4 секунд.

Я ожидаю, что когда это произойдет asyncTask.Result, если он еще не закончен, он должен подождать, пока он не закончится, и, как только это произойдет, он должен вернуть результат.

Если я перейду через отладчик, оператор SQL завершится и лямбда-функция завершится, но return result;строка GetTotalAsyncникогда не будет достигнута.

Есть идеи, что я делаю не так?

Есть предложения, где мне нужно изучить, чтобы это исправить?

Может быть, это где-то тупик, и если да, то есть ли прямой способ его найти?

Кит
источник

Ответы:

150

Ага, все в тупике. И частая ошибка с TPL, так что не расстраивайтесь.

Когда вы пишете await foo, среда выполнения по умолчанию планирует продолжение функции в том же контексте SynchronizationContext, с которого был запущен метод. По-английски, допустим, вы позвонили себе ExecuteAsyncиз потока пользовательского интерфейса. Ваш запрос выполняется в потоке пула потоков (потому что вы вызвали Task.Run), но затем вы ждете результата. Это означает, что среда выполнения будет планировать " return result;" выполнение вашей строки в потоке пользовательского интерфейса, а не в пуле потоков.

Так как же этот тупик? Представьте, что у вас есть только этот код:

var task = dataSource.ExecuteAsync(_ => 42);
var result = task.Result;

Итак, первая строка запускает асинхронную работу. Вторая строка затем блокирует поток пользовательского интерфейса . Поэтому, когда среда выполнения хочет запустить строку «возврата результата» обратно в поток пользовательского интерфейса, она не может этого сделать, покаResult завершится. Но, конечно, Результат не может быть дан, пока не произойдет возврат. Тупик.

Это иллюстрирует ключевое правило использования TPL: когда вы используете .Resultпоток пользовательского интерфейса (или какой-либо другой причудливый контекст синхронизации), вы должны быть осторожны, чтобы убедиться, что ничего, от чего зависит задача, не запланировано для потока пользовательского интерфейса. Иначе случится зло.

Ну так что ты делаешь? Вариант №1 - везде использовать await, но, как вы сказали, это уже не вариант. Второй вариант, который вам доступен, - просто прекратить использование await. Вы можете переписать свои две функции на:

public static Task<T> ExecuteAsync<T>(this OurDBConn dataSource, Func<OurDBConn, T> 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<ResultClass> GetTotalAsync( ... )
{
    return this.DBConnection.ExecuteAsync<ResultClass>(
        ds => ds.Execute("select slow running data into result"));
}

Какая разница? Теперь нигде нет ожидания, поэтому в потоке пользовательского интерфейса ничего не запланировано. Для таких простых методов, как эти, которые имеют единственный возврат, нет смысла делать "var result = await...; return result шаблон "; просто удалите модификатор async и передайте объект задачи напрямую. Это меньше накладных расходов, если ничего другого.

Вариант № 3 состоит в том, чтобы указать, что вы не хотите, чтобы ваши ожидания возвращались обратно в поток пользовательского интерфейса, а просто планируете в пул потоков. Вы делаете это с помощью ConfigureAwaitметода, например:

public static async Task<ResultClass> GetTotalAsync( ... )
{
    var resultTask = this.DBConnection.ExecuteAsync<ResultClass>(
        ds => return ds.Execute("select slow running data into result");

    return await resultTask.ConfigureAwait(false);
}

Ожидание задачи обычно запланировано для потока пользовательского интерфейса, если вы в ней; ожидание результата ContinueAwaitпроигнорирует любой контекст, в котором вы находитесь, и всегда будет планировать пул потоков. Обратной стороной этого является то, что вам придется разбрызгивать это повсюду во всех функциях, от которых зависит ваш .Result, потому что любое пропущенное .ConfigureAwaitможет быть причиной другого тупика.

Джейсон Малиновски
источник
6
Кстати, вопрос касается ASP.NET, поэтому нет потока пользовательского интерфейса. Но проблема с взаимоблокировками точно такая же из-за ASP.NET SynchronizationContext.
svick
Это многое объясняло, так как у меня был похожий код .Net 4, в котором не было проблем, но который использовал TPL без ключевых слов async/ await.
Кейт
2
TPL = Task Parallel Library msdn.microsoft.com/en-us/library/dd460717(v=vs.110).aspx
Джейми
Если кто-то ищет код VB.net (например, я), он объясняется здесь: docs.microsoft.com/en-us/dotnet/visual-basic/programming-guide/…
MichaelDarkBlue
Не могли бы вы помочь мне в stackoverflow.com/questions/54360300/…
Джитендра Панчоли
36

Это классический asyncсценарий смешанного тупика, который я описываю в своем блоге . Джейсон хорошо это описал: по умолчанию «контекст» сохраняется при каждом awaitи используется для продолжения asyncметода. Этот «контекст» является текущим, SynchronizationContextесли он nullне является текущим TaskScheduler. Когда asyncметод пытается продолжить, он сначала повторно вводит захваченный «контекст» (в данном случае ASP.NET SynchronizationContext). ASP.NET SynchronizationContextразрешает только один поток в контексте за раз, и в этом контексте уже есть поток - поток заблокирован Task.Result.

Есть два совета, которые помогут избежать этого тупика:

  1. Используйте asyncдо конца. Вы упомянули, что «не можете» этого сделать, но я не уверен, почему. ASP.NET MVC на .NET 4.5, безусловно, может поддерживать asyncдействия, и внести это несложно.
  2. Используйте ConfigureAwait(continueOnCapturedContext: false)как можно больше. Это переопределяет поведение по умолчанию возобновления в захваченном контексте.
Стивен Клири
источник
Есть ли ConfigureAwait(false)гарантия того, что текущая функция продолжается на другом контексте?
chue x
Платформа MVC поддерживает его, но это часть существующего приложения MVC с большим количеством уже существующих JS на стороне клиента. Я не могу легко переключиться на asyncдействие, не нарушив того, как это работает на стороне клиента. Я определенно планирую изучить этот вариант в более долгосрочной перспективе.
Кейт
Просто чтобы прояснить мой комментарий - мне было любопытно ConfigureAwait(false), решило бы использование дерева вызовов проблему OP.
chue x
3
@Keith: выполнение действия MVC asyncвообще не влияет на клиентскую сторону. Я объясняю это в другом сообщении блога, asyncне меняет протокол HTTP .
Стивен Клири
1
@Keith: Это нормально, async"расти" через кодовую базу. Если ваш метод контроллера может зависеть от асинхронных операций, тогда должен возвращаться метод базового класса Task<ActionResult>. asyncВсегда неудобно переходить к большому проекту, потому что смешивание asyncи синхронизация кода сложны и запутаны. Чистый asyncкод намного проще.
Стивен Клири
12

Я был в той же ситуации взаимоблокировки, но в моем случае, вызывая метод 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
}

это хороший подход, любая идея?

Данилов
источник
Это решение тоже работает у меня, но я не уверен, что это хорошее решение или оно может где-то сломаться. Кто
Константин Вдовкин
ну, наконец, я
выбрал
1
Я думаю, что использование Task.Run снижает производительность. В моем тестировании Task.Run почти удваивает время выполнения HTTP-запроса 100 мс.
Тимоти Гонсалес
1
это имеет смысл, вы создаете новую задачу для упаковки асинхронного вызова, производительность - это компромисс
Данилов,
Фантастически это сработало и для меня, мой случай также был вызван синхронным методом, вызывающим асинхронный. Спасибо!
Леонардо Спина
4

Просто чтобы добавить к принятому ответу (недостаточно репутации для комментариев), у меня возникла эта проблема при блокировке using task.Result, событие, хотя каждое из них было awaitниже ConfigureAwait(false), как в этом примере:

public Foo GetFooSynchronous()
{
    var foo = new Foo();
    foo.Info = GetInfoAsync.Result;  // often deadlocks in ASP.NET
    return foo;
}

private async Task<string> GetInfoAsync()
{ 
    return await ExternalLibraryStringAsync().ConfigureAwait(false);
}

На самом деле проблема заключалась в коде внешней библиотеки. Метод библиотеки async пытался продолжить работу в контексте синхронизации вызова, независимо от того, как я настроил ожидание, что привело к тупиковой ситуации.

Таким образом, ответ заключался в том, чтобы развернуть мою собственную версию кода внешней библиотеки 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<string> 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<string> GetInfoAsync
{
    return await ExternalLibraryStringAsync().ConfigureAwait(false);
}
Кэмерон Джефферс
источник
За что проголосовали против? Это решение работает для меня.
Кэмерон Джефферс,
Вы возвращаете объект до того, как Taskон был завершен, и не предоставляете вызывающей стороне никаких средств определения, когда действительно происходит мутация возвращенного объекта.
Servy
хм да, понятно. Так следует ли мне использовать какой-то метод «подождать, пока задача не завершится», который использует цикл while с ручной блокировкой (или что-то в этом роде)? Или упаковать в GetFooSynchronousметод такой блок ?
Кэмерон Джефферс
1
Если вы это сделаете, это будет тупик. Вам нужно выполнить полную асинхронность, вернув Taskвместо блокировки.
Servy
К сожалению, это не вариант, класс реализует синхронный интерфейс, который я не могу изменить.
Кэмерон Джефферс,
0

быстрый ответ: измените эту строку

ResultClass slowTotal = asyncTask.Result;

к

ResultClass slowTotal = await asyncTask;

Зачем? вы не должны использовать .result, чтобы получить результат задач внутри большинства приложений, кроме консольных приложений, если вы это сделаете, ваша программа будет зависать, когда доберется туда

вы также можете попробовать приведенный ниже код, если хотите использовать .Result

ResultClass slowTotal = Task.Run(async ()=>await asyncTask).Result;
Рамин
источник