Виклик асинхронних методів з неасинхронного коду


75

Я в процесі оновлення бібліотеки, яка має поверхню API, вбудовану в .NET 3.5. В результаті всі методи є синхронними. Я не можу змінити API (тобто перетворити повернені значення в Завдання), тому що для цього потрібно буде змінити всі абоненти. Тож я залишаюся з тим, як найкраще викликати асинхронні методи синхронно. Це стосується консольних програм ASP.NET 4, ASP.NET Core та .NET / .NET Core.

Можливо, я був недостатньо чітким - ситуація полягає в тому, що я маю існуючий код, який не знає асинхронізацію, і я хочу використовувати нові бібліотеки, такі як System.Net.Http та AWS SDK, які підтримують лише асинхронні методи. Тому мені потрібно подолати розрив і мати можливість мати код, який можна викликати синхронно, але потім можна викликати асинхронні методи в іншому місці.

Я багато читав, і неодноразово про це запитувались і відповідали.

Виклик асинхронного методу з неасинхронного методу

Синхронно чекаючи асинхронної операції, і чому функція Wait () заморожує програму тут

Виклик асинхронного методу з синхронного методу

Як запустити метод асинхронного завдання <T> синхронно?

Виклик асинхронного методу синхронно

Як викликати асинхронний метод із синхронного методу в C #?

Проблема в тому, що більшість відповідей різні! Найпоширеніший підхід, який я бачив, - використання .Result, але це може зайти в глухий кут. Я спробував усі наступні дії, і вони працюють, але я не впевнений, що найкращий підхід уникнути тупикових ситуацій, мати хорошу продуктивність і чудово грати з робочим середовищем (з точки зору вшанування планувальників завдань, опцій створення завдань тощо). ). Чи існує остаточна відповідь? Який найкращий підхід?

private static T taskSyncRunner<T>(Func<Task<T>> task)
    {
        T result;
        // approach 1
        result = Task.Run(async () => await task()).ConfigureAwait(false).GetAwaiter().GetResult();

        // approach 2
        result = Task.Run(task).ConfigureAwait(false).GetAwaiter().GetResult();

        // approach 3
        result = task().ConfigureAwait(false).GetAwaiter().GetResult();

        // approach 4
        result = Task.Run(task).Result;

        // approach 5
        result = Task.Run(task).GetAwaiter().GetResult();


        // approach 6
        var t = task();
        t.RunSynchronously();
        result = t.Result;

        // approach 7
        var t1 = task();
        Task.WaitAll(t1);
        result = t1.Result;

        // approach 8?

        return result;
    }

5
Відповідь - ви цього не робите. Ви додаєте нові асинхронні методи, а старі синхронні зберігаєте там для старих абонентів.
Скотт Чемберлен,

14
Це здається трохи жорстким і справді вбиває здатність використовувати новий код. Наприклад, нова версія AWS SDK не має несинхронних методів. Те саме для ряду інших сторонніх бібліотек. Отже, якщо ви не перепишете світ, ви не зможете використовувати жодного з них?
Ерік Т,

Варіант 8: Можливо, TaskCompletionSource може бути варіантом?
OrdinaryOrange

Властивість результату викликає тупикові ситуації лише при виклику, поки завдання все ще працює. Я не впевнений на 100%, але ви повинні бути в безпеці, якщо правильно почекаєте, поки завдання закінчиться, як у підході 7.
infiniteRefactor

1
Показ зразка, чому ви не можете використовувати синхронний API з асинхронного коду, може допомогти знайти кращу відповідь, ніж на @scott
Олексій Левенков

Відповіді:


90

Тож я залишаюся з тим, як найкраще викликати асинхронні методи синхронно.

По-перше, це нормально робити. Я заявляю це, тому що на Stack Overflow часто вказують на це як на вчинок диявола як на загальну заяву без урахування конкретної справи.

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

Тупикові ситуації походять від двох потоків, які намагаються одночасно ввести той самий однопотоковий контекст синхронізації. Будь-яка техніка, яка уникає цього, надійно уникає тупикових ситуацій, спричинених блокуванням.

Тут усі ваші дзвінки .ConfigureAwait(false)безглузді, тому що ви їх не чекаєте.

RunSynchronously недійсний для використання, оскільки не всі завдання можуть бути оброблені таким чином.

.GetAwaiter().GetResult()відрізняється від Result/Wait()того, що імітуєawait поведінку розповсюдження винятків. Вам потрібно вирішити, хочете ви цього чи ні. (Тож досліджуйте, що це за поведінка; тут не потрібно цього повторювати).

Крім того, усі ці підходи мають схожу ефективність. Вони так чи інакше розподілять подію ОС і блокують її. Це дорога частина. Не знаю, який підхід є абсолютно дешевим.

Мені особисто подобається Task.Run(() => DoSomethingAsync()).Wait();зразок, оскільки він категорично уникає тупикових ситуацій, простий і не приховує деяких винятків, які GetResult()можуть приховувати. Але ви можете використовувати GetResult()і з цим.


3
Дякую, це має великий сенс.
Ерік Т,

42

Я в процесі оновлення бібліотеки, яка має поверхню API, вбудовану в .NET 3.5. В результаті всі методи є синхронними. Я не можу змінити API (тобто перетворити повернені значення в Завдання), тому що для цього потрібно буде змінити всі абоненти. Тож я залишаюся з тим, як найкраще викликати асинхронні методи синхронно.

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

Я рекомендую вам зберегти старі синхронні API, а потім запровадити асинхронні API поряд з ними. Ви можете зробити це за допомогою "логічного аргументу", як описано в моїй статті MSDN про Brownfield Async .

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

  1. ConfigureAwaitмає сенс лише тоді, коли є await; інакше це нічого не робить.
  2. Resultоберне винятки у AggregateException; якщо вам потрібно заблокувати, використовуйте GetAwaiter().GetResult()замість цього.
  3. Task.Runбуде виконувати свій код на потоці пулу потоків (очевидно). Це нормально, лише якщо код може працювати на потоці пулу потоків.
  4. RunSynchronously- це вдосконалений API, який використовується в надзвичайно рідкісних ситуаціях при виконанні динамічного паралелізму на основі завдань. Ви зовсім не в такому сценарії.
  5. Task.WaitAllз одним завданням те саме, що і просто Wait().
  6. async () => await xце просто менш ефективний спосіб сказати () => x.
  7. Блокування завдання, запущеного з поточного потоку, може спричинити тупикові ситуації .

Ось розбивка:

// Problems (1), (3), (6)
result = Task.Run(async () => await task()).ConfigureAwait(false).GetAwaiter().GetResult();

// Problems (1), (3)
result = Task.Run(task).ConfigureAwait(false).GetAwaiter().GetResult();

// Problems (1), (7)
result = task().ConfigureAwait(false).GetAwaiter().GetResult();

// Problems (2), (3)
result = Task.Run(task).Result;

// Problems (3)
result = Task.Run(task).GetAwaiter().GetResult();

// Problems (2), (4)
var t = task();
t.RunSynchronously();
result = t.Result;

// Problems (2), (5)
var t1 = task();
Task.WaitAll(t1);
result = t1.Result;

Замість будь-якого з цих підходів, оскільки у вас є діючий, що працює синхронний код , вам слід використовувати його поряд із новим природно-асинхронним кодом. Наприклад, якщо використовувався ваш існуючий код WebClient:

public string Get()
{
  using (var client = new WebClient())
    return client.DownloadString(...);
}

і ви хочете додати async API, тоді я б зробив це так:

private async Task<string> GetCoreAsync(bool sync)
{
  using (var client = new WebClient())
  {
    return sync ?
        client.DownloadString(...) :
        await client.DownloadStringTaskAsync(...);
  }
}

public string Get() => GetCoreAsync(sync: true).GetAwaiter().GetResult();

public Task<string> GetAsync() => GetCoreAsync(sync: false);

або, якщо вам потрібно використовувати HttpClientз якихось причин:

private string GetCoreSync()
{
  using (var client = new WebClient())
    return client.DownloadString(...);
}

private static HttpClient HttpClient { get; } = ...;

private async Task<string> GetCoreAsync(bool sync)
{
  return sync ?
      GetCoreSync() :
      await HttpClient.GetString(...);
}

public string Get() => GetCoreAsync(sync: true).GetAwaiter().GetResult();

public Task<string> GetAsync() => GetCoreAsync(sync: false);

При такому підході ваша логіка піде на Coreметоди, які можуть виконуватися синхронно або асинхронно (як визначається syncпараметром). Якщо syncє true, то основні методи повинні повертати вже завершену завдання. Для реалізації використовуйте синхронні API для синхронного запуску та асинхронні API для запуску асинхронно.

Зрештою, я рекомендую припинити роботу синхронних API.


Чи можете ви пояснити докладніше пункт шостий?
Емерсон Соарес

@EmersonSoares: Як я пояснював у своєму асинхронному вступі , ви можете awaitотримати результат методу, оскільки він повертається Task, а не тому, що він async. Це означає, що для тривіальних методів ви можете відмовитись від ключових слів .
Стівен Клірі

Нещодавно я зіткнувся з такою ж проблемою в asp.net mvc 5 + EF6. Я використовував TaskFactory, оскільки ця відповідь пропонує stackoverflow.com/a/25097498/1683040 , і це зробило для мене фокус :), але не впевнений у інших сценаріях.
LeonardoX

Я розумію вашу пропозицію, використовуючи WebClient. Але яку користь дає метод Core при використанні HttpClient? Чи не буде чистішим та ефективнішим, якщо синхронний метод безпосередньо викликає GetCoreSyncметод, залишаючи в цьому випадку логічний аргумент?
Кріпін

@Creepin: Так, я так вважаю.
Стівен Клірі

1

Я щойно пройшов через цю штуку за допомогою AWS S3 SDK. Раніше це була синхронізація, і я створив для цього купу коду, але зараз це асинхронізація. І це прекрасно: вони змінили це, нічого не можна отримати, стогнучи про це, рухайтесь далі.
Тому мені потрібно оновити програму, і мої варіанти полягають у тому, щоб або переформатувати значну частину мого додатка, щоб бути асинхронним, або "зламати" асинхронний API S3, щоб він поводився як синхронізація.
Врешті-решт я доберусь до більшого асинхронного рефакторингу - є багато переваг - але сьогодні у мене є більша риба для смаження, тому я вирішив підробити синхронізацію.

Оригінальний код синхронізації був
ListObjectsResponse response = api.ListObjects(request);
і дуже простий асинхронної еквівалент , який працює для мене це
Task<ListObjectsV2Response> task = api.ListObjectsV2Async(rq2);
ListObjectsV2Response rsp2 = task.GetAwaiter().GetResult();

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


-3

Ви можете викликати метод асинхронізації з несинхронного методу. Перевірте код нижче.

     public ActionResult Test()
     {
        TestClass result = Task.Run(async () => await GetNumbers()).GetAwaiter().GetResult();
        return PartialView(result);
     }

    public async Task<TestClass> GetNumbers()
    {
        TestClass obj = new TestClass();
        HttpResponseMessage response = await APICallHelper.GetData(Functions.API_Call_Url.GetCommonNumbers);
        if (response.IsSuccessStatusCode)
        {
            var result = response.Content.ReadAsStringAsync().Result;
            obj = JsonConvert.DeserializeObject<TestClass>(result);
        }
        return obj;
    }
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.