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


у меня есть многоуровневое приложение .Net 4.5, вызывающее метод с помощью C#'s 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 никогда не достигается.

есть идеи, что я делаю неправильно?

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

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

4 84

4 ответа:

Да, это тупик все в порядке. И распространенная ошибка с ТПЛ, так что не расстраивайтесь.

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

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

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

Итак, первая строка запускает асинхронную работу. Вторая строка тогда блокирует поток пользовательского интерфейса. Поэтому, когда среда выполнения хочет запустить строку "return 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" pattern; просто удалите модификатор 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 будет игнорировать любой контекст, в котором вы находитесь, и всегда планировать в пул потоков. Недостатком этого является то, что вы должны посыпать это везде во всех функциях своего .Результат зависит от того, потому что любой пропустил .ConfigureAwait может быть причиной очередного тупика.

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

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

  1. использовать async полностью вниз. Вы упомянули, что "не можете" этого сделать, но я не уверен, почему нет. ASP.NET MVC на .NET 4.5, безусловно, может поддерживать async действия,и это не трудно изменить.
  2. использовать ConfigureAwait(continueOnCapturedContext: false) как можно больше. По умолчанию поведение возобновления в захваченном контексте.

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

private static SiteMetadataCacheItem GetCachedItem()
{
      TenantService TS = new TenantService(); // my service datacontext
      var CachedItem = Task.Run(async ()=> 
               await TS.GetTenantDataAsync(TenantIdValue)
      ).Result; // dont deadlock anymore
}

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

просто чтобы добавить к принятому ответу (недостаточно rep для комментариев), у меня возникла эта проблема при блокировке с помощью 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 для 'deadlock'). Он вращается вокруг использования 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);
}