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


231

У мене є asyncметод:

public async Task<string> GenerateCodeAsync()
{
    string code = await GenerateCodeService.GenerateCodeAsync();
    return code;
}

Мені потрібно викликати цей метод із синхронного методу.

Як я можу це зробити без копіювання GenerateCodeAsyncметоду для того, щоб це працювало синхронно?

Оновлення

Однак розумного рішення не знайдено.

Однак я бачу, що HttpClientвже реалізує цю закономірність

using (HttpClient client = new HttpClient())
{
    // async
    HttpResponseMessage responseAsync = await client.GetAsync(url);

    // sync
    HttpResponseMessage responseSync = client.GetAsync(url).Result;
}


1
Я сподівався на більш просте рішення, думаючи, що asp.net впорається з цим набагато простіше, ніж написати стільки рядків коду
Каталін

Чому б просто не прийняти асинхронний код? В ідеалі вам потрібно більше асинхронного коду, а не менше.
Paulo Morgado

53
[Чому б просто не прийняти асинхронний код?] Ха, можливо, саме тому, що використовується асинхронний код, їм потрібно це рішення, оскільки великі частини проекту перетворюються! Ви не можете відновити Рим за день.
Микола Петерсен

1
@NicholasPetersen іноді сторонні бібліотеки можуть змусити вас це зробити. Приклад побудови динамічних повідомлень у методі WithMessage поза FluentValidation. API для асинхронізації для цього не існує через дизайн бібліотеки - перевантаження WithMessage є статичними. Інші методи передачі динамічних аргументів у WithMessage дивні.
H.Wojtowicz

Відповіді:


278

Ви можете отримати доступ до Resultвластивості завдання, що призведе до блокування потоку, поки результат не буде доступний:

string code = GenerateCodeAsync().Result;

Примітка. У деяких випадках це може призвести до глухого кута: Ваш дзвінок Resultблокує основний потік, тим самим запобігаючи виконанню залишку коду асинхронізації. Ви можете переконатись, що цього не сталося:

Це не означає, що вам слід просто бездумно додавати .ConfigureAwait(false)після всіх своїх асинхронних дзвінків! Детальний аналіз того, чому і коли слід використовувати .ConfigureAwait(false), дивіться наступну публікацію в блозі:


33
Якщо викликають resultризики в глухому куті, а потім , коли це безпечно , щоб отримати результат? Чи потрібен кожен асинхронний дзвінок Task.Runабо ConfigureAwait(false)?
Роберт Харві

4
У ASP.NET немає «головної нитки» (на відміну від програми GUI), але тупик все ще можливий через те, як AspNetSynchronizationContext.Post серіалізує продовження асинхронізації:Task newTask = _lastScheduledTask.ContinueWith(_ => SafeWrapCallback(action)); _lastScheduledTask = newTask;
noseratio

4
@RobertHarvey: Якщо у вас немає контролю над впровадженням методу асинхронізації, який ви блокуєте, тоді так, вам слід обробляти його, Task.Runщоб залишатися в безпеці. Або скористайтеся чимось на зразок WithNoContextзменшення надмірної комутації ниток.
нісраціо

10
ПРИМІТКА. Виклик .Resultвсе ще може зайти в тупиковий стан, якщо абонент знаходиться в самому пулі потоків. Візьміть сценарій, коли Пул потоку розміром 32 та 32 завдання виконуються та Wait()/Resultчекають на 33-е завдання, яке ще заплановано, яке хоче запуститись на одній з ниток очікування.
Warty

55

Вам слід отримати офіціант ( GetAwaiter()) і закінчити очікування завершення асинхронного завдання ( GetResult()).

string code = GenerateCodeAsync().GetAwaiter().GetResult();

37
Ми зіткнулися з тупиками за допомогою цього рішення. Будьте попереджені.
Олівер

6
MSDNTask.GetAwaiter : Цей метод призначений для використання компілятора, а не для використання в коді програми.
foka

У мене все ще з’явилося спливаюче вікно діалогу помилок (проти моєї волі) за допомогою кнопок «Перейти до» або «Повторити»…. однак виклик насправді виконується і повертається з належною відповіддю.
Джонатан Хансен

30

Ви повинні мати можливість це зробити, використовуючи делегатів, лямбда-вираз

private void button2_Click(object sender, EventArgs e)
    {

        label1.Text = "waiting....";

        Task<string> sCode = Task.Run(async () =>
        {
            string msg =await GenerateCodeAsync();
            return msg;
        });

        label1.Text += sCode.Result;

    }

    private Task<string> GenerateCodeAsync()
    {
        return Task.Run<string>(() => GenerateCode());
    }

    private string GenerateCode()
    {
        Thread.Sleep(2000);
        return "I m back" ;
    }

Цей фрагмент не збирається. Тип повернення від Task.Run - Завдання. Дивіться цей блог MSDN для повного пояснення.
Звернення

5
Дякую за вказівку, так, вона повертає тип завдання. Заміна "string sCode" на Task <string> або var sCode повинна вирішити її. Додавання повного коду компіляції для зручності.
Faiyaz

20

Мені потрібно викликати цей метод із синхронного методу.

Це можливо за допомогою GenerateCodeAsync().ResultабоGenerateCodeAsync().Wait() , як підказує інша відповідь. Це заблокує поточний потік, поки GenerateCodeAsyncне завершиться.

Однак ваше питання позначено тегом , і ви також залишили коментар:

Я сподівався на більш просте рішення, думаючи, що asp.net впорається з цим набагато простіше, ніж написати стільки рядків коду

Моя думка, ви не повинні блокувати асинхронний метод в ASP.NET. Це зменшить масштабованість вашого веб-програми та може створити глухий кут (коли повідомлення розміщено awaitвсередині ). Використання для завантаження чого-небудь на нитку пулу, а потім блокування ще більше зашкодить масштабованості, оскільки це вимагає ще +1 потік для обробки заданого HTTP-запиту.GenerateCodeAsyncAspNetSynchronizationContextTask.Run(...).Result

ASP.NET має вбудовану підтримку асинхронних методів або через асинхронні контролери (в ASP.NET MVC та Web API), або безпосередньо через AsyncManagerта PageAsyncTaskв класичному ASP.NET. Ви повинні використовувати його. Для отримання більш детальної інформації перевірте цю відповідь .


Я перезаписую SaveChanges()метод DbContext, і ось я закликаю методи асинхронізації, тому, на жаль, диспетчер асинхрон не допоможе мені в цій ситуації
Catalin

3
@RaraituL, загалом, ви не змішуєте код асинхронізації та синхронізації, вибираєте ефірну модель. Ви можете реалізувати і те, SaveChangesAsyncі SaveChangesлише переконайтесь, що вони не викликають обох в одному проекті ASP.NET.
носраціо

4
Не всі .NET MVCфільтри підтримують асинхронний код, наприклад IAuthorizationFilter, тому я не можу використовувати asyncвесь шлях
Каталін

3
@Noseratio - це нереальна мета. Занадто багато бібліотек з асинхронним та синхронним кодом, а також ситуацій, коли використання лише однієї моделі неможливо. MVC ActionFilters, наприклад, не підтримує асинхронний код.
Джастін Скілз

9
@Noserato, питання стосується виклику асинхронного методу із синхронного. Іноді ви не можете змінити API, який ви впроваджуєте. Скажімо, ви реалізуєте якийсь синхронний інтерфейс з якоїсь третьої фреймворку партії "A" (ви не можете переписати фреймворк асинхронним способом), але стороння бібліотека "B", яку ви намагаєтеся використовувати у своїй реалізації, має лише асинхронний характер. Отриманий продукт також є бібліотекою, і його можна використовувати в будь-якому місці, включаючи ASP.NET тощо
dimzon

19

Microsoft Identity має методи розширення, які викликають методи асинхронізації синхронно. Наприклад, існує метод GenerateUserIdentityAsync () і рівний CreateIdentity ()

Якщо ви подивитеся на UserManagerExtensions.CreateIdentity (), це виглядає приблизно так:

 public static ClaimsIdentity CreateIdentity<TUser, TKey>(this UserManager<TUser, TKey> manager, TUser user,
        string authenticationType)
        where TKey : IEquatable<TKey>
        where TUser : class, IUser<TKey>
    {
        if (manager == null)
        {
            throw new ArgumentNullException("manager");
        }
        return AsyncHelper.RunSync(() => manager.CreateIdentityAsync(user, authenticationType));
    }

Тепер давайте подивимося, що робить AsyncHelper.RunSync

  public static TResult RunSync<TResult>(Func<Task<TResult>> func)
    {
        var cultureUi = CultureInfo.CurrentUICulture;
        var culture = CultureInfo.CurrentCulture;
        return _myTaskFactory.StartNew(() =>
        {
            Thread.CurrentThread.CurrentCulture = culture;
            Thread.CurrentThread.CurrentUICulture = cultureUi;
            return func();
        }).Unwrap().GetAwaiter().GetResult();
    }

Отже, це ваша обгортка для асинхронного методу. І, будь ласка, не читайте дані з Result - це потенційно заблокує ваш код у ASP.

Є й інший спосіб - який для мене підозрілий, але ви також можете це врахувати

  Result r = null;

            YourAsyncMethod()
                .ContinueWith(t =>
                {
                    r = t.Result;
                })
                .Wait();

3
Що ви вважаєте проблемою із другого запропонованого вами способу?
Девід Кларк

@DavidClarke, ймовірно, проблема безпеки потоку доступу до енергонезалежної змінної з декількох потоків без блокування.
Теодор Зуліяс

9

Щоб запобігти тупикам, я завжди намагаюся використовувати Task.Run() коли мені потрібно синхронно викликати метод асинхронізації, який згадує @Heinzi.

Однак метод повинен бути змінений, якщо метод асинхронізації використовує параметри. Наприклад, Task.Run(GenerateCodeAsync("test")).Resultнаведена помилка:

Аргумент 1: не вдається перетворити з " System.Threading.Tasks.Task<string>" в "System.Action"

Це можна назвати так:

string code = Task.Run(() => GenerateCodeAsync("test")).Result;

6

Більшість відповідей у ​​цій темі або складні, або призведуть до тупикової ситуації.

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

var task = Task.Run(() => GenerateCodeAsync()); 
task.Wait();
string code = task.Result;

Крім того, тут посилання на статтю MSDN, яка говорить про абсолютно те саме - https://blogs.msdn.microsoft.com/jpsanders/2017/08/28/asp-net-do-not-use-task-result- в основному-контексті /


1

Я віддаю перевагу не блокуючий підхід:

            Dim aw1=GenerateCodeAsync().GetAwaiter()
            While Not aw1.IsCompleted
                Application.DoEvents()
            End While

0

Я використовую такий підхід:

    private string RunSync()
    {
        var task = Task.Run(async () => await GenerateCodeService.GenerateCodeAsync());
        if (task.IsFaulted && task.Exception != null)
        {
            throw task.Exception;
        }

        return task.Result;
    }

-1

Інший спосіб може бути, якщо ви хочете зачекати, поки завдання не буде закінчено:

var t = GenerateCodeService.GenerateCodeAsync();
Task.WhenAll(t);
string code = t.Result;

1
Це очевидно неправильно. WhenAll також повертає завдання, якого ви не чекаєте.
Роберт Шмідт

-1

Редагувати:

Завдання має метод Wait, Task.Wait (), який чекає, коли «обіцянка» розв’яжеться, а потім продовжить, тим самим зробивши її синхронною. приклад:


async Task<String> MyAsyncMethod() { ... }

String mySyncMethod() {

    return MyAsyncMethod().Wait();
}

3
Будь ласка, детально деталізуйте свою відповідь. Як він використовується? Наскільки конкретно це допомагає відповісти на запитання?
Scratte

-2

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

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