Вызов асинхронного метода синхронно

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 справится с этим гораздо проще, чем написание стольких строк кода
Catalin
Почему бы просто не принять асинхронный код? В идеале вы хотели бы больше асинхронного кода, а не меньше.
Пауло Моргадо
54
[Почему бы просто не принять асинхронный код?] Ха, может быть, именно потому, что кто-то использует асинхронный код, он нуждается в этом решении, поскольку большие части проекта преобразуются! Вы не можете восстановить Рим за один день.
Николас Петерсен
1
@NicholasPetersen иногда сторонняя библиотека может заставить вас сделать это. Пример построения динамических сообщений в методе WithMessage из FluentValidation. Для этого не существует асинхронного API из-за дизайна библиотеки - перегрузки WithMessage являются статическими. Другие методы передачи динамических аргументов в WithMessage странные.
Х. Войтович

Ответы:

278

Вы можете получить доступ к Resultсвойству задачи, что приведет к блокировке вашего потока, пока не станет доступен результат:

string code = GenerateCodeAsync().Result;

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

Это не значит, что вы должны просто бездумно добавлять .ConfigureAwait(false)после всех ваших асинхронных вызовов! Для подробного анализа того, почему и когда вы должны использовать .ConfigureAwait(false), см. Следующий пост в блоге:

Heinzi
источник
33
Если вызывающие resultриски в тупике, а затем , когда это безопасно , чтобы получить результат? Каждый асинхронный вызов требует Task.Runили ConfigureAwait(false)?
Роберт Харви
4
В ASP.NET нет «основного потока» (в отличие от приложения с графическим интерфейсом), но тупик все еще возможен из-за того, как AspNetSynchronizationContext.Post сериализует асинхронные продолжения:Task newTask = _lastScheduledTask.ContinueWith(_ => SafeWrapCallback(action)); _lastScheduledTask = newTask;
noseratio
4
@RobertHarvey: Если у вас нет контроля над реализацией асинхронного метода, для которого вы блокируете, то да, вы должны обернуть его, Task.Runчтобы оставаться в безопасности. Или используйте что-то вроде WithNoContextсокращения избыточных потоков.
Носовое отношение
10
ПРИМЕЧАНИЕ. Вызов .Resultможет оставаться в тупике, если вызывающий абонент находится в самом пуле потоков. Возьмем сценарий, в котором пул потоков имеет размер 32, и 32 задачи выполняются и Wait()/Resultожидают запланированной 33-й задачи, которая должна быть запущена в одном из ожидающих потоков.
Бородавки
55

Вы должны получить awaiter ( GetAwaiter()) и завершить ожидание завершения асинхронной задачи ( GetResult()).

string code = GenerateCodeAsync().GetAwaiter().GetResult();
Диего Торрес
источник
38
Мы столкнулись с тупиками, используя это решение. Имейте в виду.
Оливер
6
MSDNTask.GetAwaiter : этот метод предназначен для использования компилятором, а не для использования в коде приложения.
Фока
Я все еще получаю всплывающее окно с диалоговым окном ошибки (против моей воли) с кнопками «Переключиться на» или «Повторить»…. однако вызов фактически выполняется и возвращается с правильным ответом.
Джонатан Хансен
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" ;
    }
Faiyaz
источник
Этот фрагмент не будет компилироваться. Тип возврата из Task.Run - Task. Посмотрите этот блог MSDN для полного объяснения.
Appetere
5
Спасибо за указание, да, он возвращает тип задачи. Замена "string sCode" на Task <string> или var sCode должна разрешить это. Добавление полного кода компиляции для простоты.
Файаз
20

Мне нужно вызвать этот метод из синхронного метода.

Это возможно с GenerateCodeAsync().Resultили GenerateCodeAsync().Wait(), как предполагает другой ответ. Это заблокирует текущий поток, пока GenerateCodeAsyncне завершится.

Тем не менее, ваш вопрос помечен как и вы также оставили комментарий:

Я надеялся на более простое решение, полагая, что asp.net справится с этим гораздо проще, чем при написании такого количества строк кода.

Моя точка зрения, вы не должны блокировать асинхронный метод в ASP.NET. Это уменьшит масштабируемость вашего веб-приложения и может создать тупик (когда awaitпродолжение GenerateCodeAsyncразмещено внутри AspNetSynchronizationContext). Использование Task.Run(...).Resultдля разгрузки чего-либо в поток пула, а затем его блокирование еще больше повредит масштабируемости, поскольку для обработки данного HTTP-запроса потребуется еще +1 поток.

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

noseratio
источник
Я перезаписываю SaveChanges()метод DbContext, и здесь я вызываю асинхронные методы, поэтому, к сожалению, асинхронный контроллер не поможет мне в этой ситуации
Catalin
3
@RaraituL, в общем, вы не смешиваете асинхронный и синхронизирующий код, выбираете любую модель. Вы можете реализовать оба варианта SaveChangesAsyncи SaveChangesпросто убедиться, что они не вызываются в одном и том же проекте ASP.NET.
сборник носов
4
.NET MVCНапример IAuthorizationFilter, не все фильтры поддерживают асинхронный код, поэтому я не могу использовать его asyncполностью
Catalin
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;
Ogglas
источник
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- в-основного контекста /

kamalpreet
источник
1

Я предпочитаю неблокирующий подход:

            Dim aw1=GenerateCodeAsync().GetAwaiter()
            While Not aw1.IsCompleted
                Application.DoEvents()
            End While
Zibri
источник
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;
frablaser
источник
1
Это совершенно неправильно. WhenAll также возвращает задачу, которую вы не ожидаете.
Роберт Шмидт
-1

РЕДАКТИРОВАТЬ:

Задача имеет метод Wait, Task.Wait (), который ожидает разрешения «обещания», а затем продолжает работу, что делает его синхронным. пример:


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

String mySyncMethod() {

    return MyAsyncMethod().Wait();
}
Ави Чува
источник
3
Пожалуйста, уточните ваш ответ. Как это используется? Как конкретно это поможет ответить на вопрос?
Scratte
-2

Если у вас есть асинхронный метод RefreshList , вы можете вызвать этот асинхронный метод из неасинхронного метода, как показано ниже.

Task.Run(async () => { await RefreshList(); });
dush88c
источник