Чому ця дія асинхронізації висить?


102

У мене багаторівневий додаток .Net 4.5, який викликає метод, використовуючи нове asyncі awaitключове слово C #, яке просто висить, і я не можу зрозуміти, чому.

Внизу у мене є метод асинхронізації, який розширює нашу утиліту бази даних 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 - використання чекайте скрізь, але, як ви сказали, це вже не варіант. Другий доступний для вас варіант - це просто перестати використовувати функцію очікування. Ви можете переписати дві функції:

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" шаблон; просто видаліть модифікатор асинхронізації та безпосередньо передайте об'єкт завдання. Це менше накладних витрат, якщо нічого іншого.

Варіант №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може бути причиною чергового тупику.


6
До речі, питання стосується ASP.NET, тому потоку інтерфейсу немає. Але проблема з тупиками точно така ж, через ASP.NET SynchronizationContext.
svick

Це багато що пояснило, оскільки у мене був подібний код .Net 4, у якого не було проблеми, але він використовував TPL без async/ awaitключових слів.
Кіт

2
TPL = Паралельна бібліотека завдань msdn.microsoft.com/en-us/library/dd460717(v=vs.110).aspx
Jamie Ide

Якщо хтось шукає код 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)дерева викликів вирішило б проблему ОП.
chue x

3
@Keith: Здійснення дії MVC asyncзовсім не впливає на сторону клієнта. Я пояснюю це в іншій публікації блогу, asyncне змінює протокол HTTP .
Стівен Клірі

1
@Keith: Це нормально для async"зростання" через кодову базу. Якщо ваш метод контролера може залежати від асинхронних операцій, тоді метод базового класу повинен повернутися Task<ActionResult>. Перехід великого проекту на asyncзавжди незручний, оскільки змішування asyncта синхронізацію коду є складним та складним. Чистий asyncкод набагато простіше.
Стівен Клірі

12

Я опинився в тій самій тупиковій ситуації, але в моєму випадку викликав метод асинхронізації з методу синхронізації, що для мене працює:

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 майже вдвічі збільшив час виконання для 100-мільймового запиту http.
Тимофій Гонсалес

1
це має сенс, ви створюєте нове завдання для завершення виклику асинхронізації, продуктивність - компроміс
Danilow

Фантастично це спрацювало і для мене, мій випадок був викликаний також синхронним методом виклику асинхронного. Дякую!
Леонардо Спіна

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);
}

Проблема фактично полягає у зовнішньому коді бібліотеки. Метод бібліотеки 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завершення, і не надаєте абоненту засобів визначення того, коли мутація повернутого об'єкта насправді відбувається.
Сервіс

хм, так, я бачу. Тож чи варто виставляти якийсь метод "дочекатися завершення завдання", який використовує блокування вручну під час циклу (або щось подібне)? Або упакувати такий блок у GetFooSynchronousметод?
Камерон Джефферс

1
Якщо ви це зробите, це буде тупик. Потрібно повністю асинхронізуватись, повертаючи Taskзамість блокування.
Сервіс

На жаль, це не варіант, клас реалізує синхронний інтерфейс, який я не можу змінити.
Камерон Джефферс

0

швидка відповідь: змінити цей рядок

ResultClass slowTotal = asyncTask.Result;

до

ResultClass slowTotal = await asyncTask;

чому? Ви не повинні використовувати .result, щоб отримати результат завдань всередині більшості програм, крім консольних програм, якщо ви зробите це, коли ваша програма зависне, коли вона потрапить

ви також можете спробувати наведений нижче код, якщо ви хочете використовувати .Result

ResultClass slowTotal = Task.Run(async ()=>await asyncTask).Result;
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.