Подтвердить что ты не робот

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

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

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

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

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(). Если я запускаю медленный метод SQL напрямую, это занимает около 4 секунд.

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

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

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

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

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

4b9b3361

Ответ 1

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

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

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

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

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

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

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

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

Ответ 2

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

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

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

Ответ 3

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

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

Ответ 4

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

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

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