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

у меня есть многоуровневое приложение .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;
}

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

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

// do other stuff that takes a few seconds

ResultClass slowTotal = asyncTask.Result;

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

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

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

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

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

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

4 ответов


Да, это действительно тупик. И распространенная ошибка с TPL, поэтому не чувствуйте себя плохо.

когда вы пишите 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; просто удалите асинхронный модификатор и передайте объект задачи напрямую. Это меньше накладных расходов, если ничего больше.

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


это классический смешанный-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) как можно больше. По умолчанию поведение возобновления в захваченном контексте.

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

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 попытался продолжить в вызывающем контексте синхронизации, независимо от того, как я настроил await, что привело к взаимоблокировке.

таким образом, ответ состоял в том, чтобы свернуть мою собственную версию внешней библиотеки код 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);
}