Приклад асинхронізації / очікування, що спричиняє глухий кут


94

Я натрапив на кілька найкращих практик асинхронного програмування з використанням ключових слів async/ awaitключових слів (я знайомлюсь із c # 5.0).

Однією з наведених порад було наступне:

Стабільність: знайте свої контексти синхронізації

... Деякі контексти синхронізації не є ретрансляційними та однопотоковими. Це означає, що в даний момент часу в контексті може бути виконана лише одна одиниця роботи. Прикладом цього є потік інтерфейсу користувача Windows або контекст запиту ASP.NET. У цих однопотокових контекстах синхронізації легко зайти в глухий кут. Якщо ви породжуєте завдання із однопотокового контексту, то почекайте його в контексті, можливо, ваш код очікування блокує фонове завдання.

public ActionResult ActionAsync()
{
    // DEADLOCK: this blocks on the async task
    var data = GetDataAsync().Result;

    return View(data);
}

private async Task<string> GetDataAsync()
{
    // a very simple async method
    var result = await MyWebService.GetDataAsync();
    return result.ToString();
}

Якщо я спробую розібрати його самостійно, основний потік виникає до нового MyWebService.GetDataAsync();, але, оскільки основний потік чекає там, він чекає результату в GetDataAsync().Result. Тим часом, скажімо, дані готові. Чому основний потік не продовжує свою логіку продовження і повертає результат рядка з GetDataAsync()?

Хтось може пояснити мені, чому у наведеному вище прикладі є глухий кут? Я абсолютно не знаю, в чому проблема ...


Ви справді впевнені, що GetDataAsync закінчує свої речі? Або він застряє, спричиняючи просто блокування, а не тупик?
Андрій

Це приклад, який був наданий. На моє розуміння, він повинен закінчити це і мати якийсь результат готовим ...
Дрор Вайс

4
Чому ти взагалі чекаєш завдання? Вам слід чекати натомість, оскільки ви в основному втратили всі переваги асинхронної моделі.
Тоні Петріна

Щоб додати до точки @ ToniPetrina, навіть без проблеми тупикової ситуації, var data = GetDataAsync().Result;це рядок коду, який ніколи не слід робити в контексті, який ви не повинні блокувати (запит інтерфейсу користувача або ASP.NET). Навіть якщо це не блокує ситуацію, воно блокує потік на невизначений час. Тому в основному це жахливий приклад. [Вам потрібно вийти з потоку інтерфейсу користувача перед тим, як виконувати подібний код, або також використовувати awaitтам, як пропонує Тоні.]
ToolmakerSteve

Відповіді:


81

Погляньте на цей приклад , Стівен має для вас чітку відповідь:

Отже, ось що відбувається, починаючи з методу верхнього рівня ( Button1_Clickдля інтерфейсу користувача / MyController.Getдля ASP.NET):

  1. Виклики методу верхнього рівня GetJsonAsync(у контексті UI / ASP.NET).

  2. GetJsonAsyncзапускає запит REST за допомогою виклику HttpClient.GetStringAsync(все ще в контексті).

  3. GetStringAsyncповертає незавершений Task, вказуючи, що запит REST не завершений.

  4. GetJsonAsyncчекає Taskповернутого до GetStringAsync. Контекст захоплюється і буде використаний для подальшого запуску GetJsonAsyncметоду пізніше. GetJsonAsyncповертає незавершене Task, вказуючи, що GetJsonAsyncметод не завершений.

  5. Метод верхнього рівня синхронно блокує Taskповернутий за допомогою GetJsonAsync. Це блокує контекстний потік.

  6. ... Зрештою, запит REST завершиться. На цьому завершено те, Taskщо було повернутоGetStringAsync .

  7. Продовження для GetJsonAsyncтепер готове до запуску, і воно чекає, поки контекст стане доступним, щоб його можна було виконати в контексті.

  8. Тупик . Метод верхнього рівня блокує потік контексту, чекає GetJsonAsyncзавершення і GetJsonAsyncчекає звільнення контексту, щоб він міг завершити. Для прикладу інтерфейсу користувача "контекст" - це контекст інтерфейсу користувача; для прикладу ASP.NET "контекстом" є контекст запиту ASP.NET. Цей тип глухого кута може бути спричинений для будь-якого "контексту".

Ще одне посилання, яке ви повинні прочитати: Чекайте, і інтерфейс, і тупикові ситуації! О Боже!


20
  • Факт 1: GetDataAsync().Result;буде запущено, коли завдання повернетьсяGetDataAsync() завершується, тим часом воно блокує потік інтерфейсу користувача
  • Факт 2: Продовження очікування (return result.ToString() ) потрапляє в чергу до потоку інтерфейсу для виконання
  • Факт 3: Завдання, повернене GetDataAsync() буде виконано, коли буде запущено його продовження в черзі
  • Факт 4: Продовження в черзі ніколи не запускається, оскільки потік інтерфейсу користувача заблокований (Факт 1)

Тупик!

Тупиковий шлях можна подолати, надавши альтернативні варіанти, щоб уникнути факту 1 або факту 2.

  • Уникайте 1,4. Замість блокування потоку користувацького інтерфейсу використовуйтеvar data = await GetDataAsync() , що дозволяє потоку інтерфейсу продовжувати працювати
  • Уникайте 2,3. Надіслати чергу продовження await до іншого потоку, який не заблокований, наприклад, use var data = Task.Run(GetDataAsync).Result, який опублікує продовження до контексту синхронізації потоку пулу потоків. Це дозволяє виконувати завдання, яке повертається GetDataAsync().

Це дуже добре пояснено у статті Стівена Туба , десь на половині шляху, де він використовує приклад DelayAsync().


Щодо, var data = Task.Run(GetDataAsync).Resultце для мене нове. Я завжди думав, що зовнішній .Resultвигляд буде легко доступний, як тільки з’явиться перший чекає GetDataAsync, так dataбуде завжди default. Цікаво.
nawfal

18

Я просто возився з цією проблемою ще раз у проекті ASP.NET MVC. Коли ви хочете викликати asyncметоди з a PartialView, вам не дозволяється робити PartialView async. Якщо ви це зробите, ви отримаєте виняток.

Ви можете використати таке просте обхідне рішення у тому випадку, коли ви хочете викликати asyncметод із методу синхронізації:

  1. Перед дзвінком очистіть SynchronizationContext
  2. Зробіть дзвінок, тут вже не буде тупику, дочекайтеся його закінчення
  3. Відновіть SynchronizationContext

Приклад:

public ActionResult DisplayUserInfo(string userName)
{
    // trick to prevent deadlocks of calling async method 
    // and waiting for on a sync UI thread.
    var syncContext = SynchronizationContext.Current;
    SynchronizationContext.SetSynchronizationContext(null);

    //  this is the async call, wait for the result (!)
    var model = _asyncService.GetUserInfo(Username).Result;

    // restore the context
    SynchronizationContext.SetSynchronizationContext(syncContext);

    return PartialView("_UserInfo", model);
}

3

Інший головний момент полягає в тому, що ви не повинні блокувати завдання і використовувати асинхронізацію до кінця, щоб запобігти тупиковим ситуаціям. Тоді це буде все асинхронне, а не синхронне блокування.

public async Task<ActionResult> ActionAsync()
{

    var data = await GetDataAsync();

    return View(data);
}

private async Task<string> GetDataAsync()
{
    // a very simple async method
    var result = await MyWebService.GetDataAsync();
    return result.ToString();
}

6
Що робити, якщо я хочу, щоб основний (UI) потік був заблокований до завершення завдання? Або, наприклад, у програмі Console? Скажімо, я хочу використовувати HttpClient, який підтримує лише асинхронізацію ... Як я можу використовувати його синхронно без ризику тупикової ситуації ? Це повинно бути можливим. Якщо WebClient може бути використаний таким чином (через наявність методів синхронізації) і працює ідеально, то чому це не можна було зробити і з HttpClient?
Декстер

Дивіться відповідь від Філіпа Нгана вище (я знаю, що це було опубліковано після цього коментаря): Чекайте продовження await до іншого потоку, який не заблокований, наприклад, використовуйте var data = Task.Run (GetDataAsync). Результат
Jeroen

@Dexter - re " Що робити, якщо я хочу, щоб основний (UI) потік був заблокований до завершення завдання? " - чи справді ви хочете, щоб потік UI був заблокований, тобто користувач не може нічого зробити, навіть скасувати - або це те, що ви не хочете продовжувати метод, у якому ви перебуваєте? "await" або "Task.ContinueWith" обробляють останній випадок.
ToolmakerSteve

@ToolmakerSteve, звичайно, я не хочу продовжувати метод. Але я просто не можу використовувати await, тому що я не можу використовувати асинхронізацію до кінця - HttpClient викликається в основному , що, звичайно, не може бути асинхронізацією. А потім я вже робив все це в додаток консолі - в цьому випадку я хочу точно колишній - я не хочу , щоб моє додаток , щоб навіть бути багато-. Заблокуйте все .
Декстер

-1

Навколо, до чого я прийшов, - використовувати Joinметод розширення завдання, перш ніж запитувати результат.

Вигляд коду такий:

public ActionResult ActionAsync()
{
  var task = GetDataAsync();
  task.Join();
  var data = task.Result;

  return View(data);
}

Де метод об’єднання:

public static class TaskExtensions
{
    public static void Join(this Task task)
    {
        var currentDispatcher = Dispatcher.CurrentDispatcher;
        while (!task.IsCompleted)
        {
            // Make the dispatcher allow this thread to work on other things
            currentDispatcher.Invoke(delegate { }, DispatcherPriority.SystemIdle);
        }
    }
}

Мені недостатньо домену, щоб побачити недоліки цього рішення (якщо такі є)

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.