Synchroniczne wywoływanie metody asynchronicznej

231

Mam asyncmetodę:

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

Muszę wywołać tę metodę z metody synchronicznej.

Jak mogę to zrobić bez konieczności duplikowania GenerateCodeAsyncmetody, aby działała synchronicznie?

Aktualizacja

Jednak nie znaleziono rozsądnego rozwiązania.

Widzę jednak, że HttpClientjuż implementuje ten wzorzec

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

    // sync
    HttpResponseMessage responseSync = client.GetAsync(url).Result;
}
Katalina
źródło
1
Miałem nadzieję na prostsze rozwiązanie, myśląc, że asp.net poradził sobie z tym znacznie łatwiej niż pisanie tak wielu linii kodu
Catalin
Dlaczego po prostu nie zastosować kodu asynchronicznego? Idealnie byłoby chcieć więcej kodu asynchronicznego, a nie mniej.
Paulo Morgado
53
[Dlaczego po prostu nie przyjąć kodu asynchronicznego?] Ha, być może właśnie dlatego, że ktoś akceptuje kod asynchroniczny, że potrzebuje tego rozwiązania, ponieważ duże części projektu zostaną przekonwertowane! Nie możesz odbudować Rzymu w ciągu jednego dnia.
Nicholas Petersen,
1
@NicholasPetersen czasem biblioteka innej firmy może cię do tego zmusić. Przykład budowania komunikatów dynamicznych w metodzie WithMessage z FluentValidation. Nie ma do tego asynchronicznego interfejsu API ze względu na projekt biblioteki - przeciążenia WithMessage są statyczne. Inne metody przekazywania dynamicznych argumentów do WithMessage są dziwne.
H.Wojtowicz

Odpowiedzi:

278

Możesz uzyskać dostęp do Resultwłaściwości zadania, co spowoduje blokowanie twojego wątku, dopóki wynik nie będzie dostępny:

string code = GenerateCodeAsync().Result;

Uwaga: W niektórych przypadkach może to prowadzić do impasu: Twoje wezwanie do Resultzablokowania głównego wątku, uniemożliwiając w ten sposób pozostałą część kodu asynchronicznego do wykonania. Masz następujące opcje, aby upewnić się, że tak się nie stanie:

To nie znaczy, że należy po prostu bezmyślnie dodać .ConfigureAwait(false)po wszystkich połączeń asynchronicznych! Aby uzyskać szczegółową analizę, dlaczego i kiedy powinieneś używać .ConfigureAwait(false), zobacz następujący post na blogu:

Heinzi
źródło
33
Jeśli wywoływanie resultgrozi impasem, to kiedy można bezpiecznie uzyskać wynik? Czy każde połączenie asynchroniczne wymaga Task.Runlub ConfigureAwait(false)?
Robert Harvey
4
W ASP.NET nie ma „głównego wątku” (w przeciwieństwie do aplikacji GUI), ale impas jest nadal możliwy ze względu na to, jak AspNetSynchronizationContext.Post szereguje asynchroniczne kontynuacje:Task newTask = _lastScheduledTask.ContinueWith(_ => SafeWrapCallback(action)); _lastScheduledTask = newTask;
noseratio
4
@RobertHarvey: Jeśli nie masz kontroli nad implementacją metody asynchronicznej, którą blokujesz, to tak, powinieneś ją owinąć, Task.Runaby zachować bezpieczeństwo. Lub użyj czegoś takiego, WithNoContextaby zmniejszyć zbędne przełączanie wątków.
noseratio
10
UWAGA: Wywołanie .Resultmoże nadal być zakleszczone, jeśli osoba dzwoniąca znajduje się w samej puli wątków. Weźmy scenariusz, w którym pula wątków ma rozmiar 32, a zadania 32 są uruchomione i Wait()/Resultoczekują na 33. zadanie, które ma zostać zaplanowane, i które ma zostać uruchomione w jednym z oczekujących wątków.
Warty,
55

Powinieneś dostać awaiter ( GetAwaiter()) i zakończyć oczekiwanie na zakończenie asynchronicznego zadania ( GetResult()).

string code = GenerateCodeAsync().GetAwaiter().GetResult();
Diego Torres
źródło
37
Za pomocą tego rozwiązania natrafiliśmy na impas. Być ostrzeżonym.
Oliver,
6
MSDNTask.GetAwaiter : Ta metoda jest przeznaczona raczej do użytku kompilatora niż do użycia w kodzie aplikacji.
foka
Nadal pojawia się okno dialogowe błędu (wbrew mojej woli), z przyciskami „Przełącz na” lub „Ponów”…. jednak połączenie faktycznie wykonuje się i wraca z prawidłową odpowiedzią.
Jonathan Hansen
30

Powinieneś być w stanie to zrobić za pomocą delegatów, wyrażenia lambda

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" ;
    }
Fajjaz
źródło
Ten fragment kodu nie zostanie skompilowany. Typ zwracany z Task.Run to Task. Zobacz blog MSDN, aby uzyskać pełne wyjaśnienie.
Appetere
5
Dzięki za wskazanie, tak, zwraca typ zadania. Zastąpienie „string sCode” Zadaniem <ciąg> lub var sCode powinno go rozwiązać. Łatwe dodawanie pełnego kodu kompilacji.
Faiyaz,
20

Muszę wywołać tę metodę z metody synchronicznej.

Jest to możliwe przy pomocy GenerateCodeAsync().Resultlub GenerateCodeAsync().Wait(), jak sugeruje inna odpowiedź. Spowodowałoby to zablokowanie bieżącego wątku, dopóki GenerateCodeAsyncnie zostanie zakończone.

Twoje pytanie jest jednak oznaczone tagiem , a także zostawiłeś komentarz:

Miałem nadzieję na prostsze rozwiązanie, myśląc, że asp.net poradził sobie z tym znacznie łatwiej niż pisanie tak wielu linii kodu

Chodzi mi o to, że nie powinieneś blokować metody asynchronicznej w ASP.NET. Spowoduje to zmniejszenie skalowalności Twojej aplikacji internetowej i może spowodować impas (gdy awaitkontynuacja wewnątrz GenerateCodeAsyncjest wysyłana do AspNetSynchronizationContext). Użycie Task.Run(...).Resultdo odciążenia wątku puli, a następnie bloku jeszcze bardziej zaszkodzi skalowalności, ponieważ powoduje +1 wątek więcej do przetworzenia danego żądania HTTP.

ASP.NET posiada wbudowane wsparcie dla metod asynchronicznych, poprzez asynchronicznych kontrolerów (w ASP.NET MVC i Web API) lub bezpośrednio AsyncManageri PageAsyncTaskw klasycznej ASP.NET. Powinieneś go użyć. Aby uzyskać więcej informacji, sprawdź tę odpowiedź .

noseratio
źródło
Nadpisuję SaveChanges()metodę DbContexti tutaj wywołuję metody asynchroniczne, więc niestety kontroler asynchroniczny nie pomoże mi w tej sytuacji
Catalin
3
@RaraituL, ogólnie rzecz biorąc, nie miksujesz kodu asynchronicznego i synchronizującego, wybierasz inny model. Możesz zaimplementować oba SaveChangesAsynci SaveChangestylko upewnić się, że nie zostaną wywołane oba w tym samym projekcie ASP.NET.
noseratio
4
Na przykład nie wszystkie .NET MVCfiltry obsługują kod asynchroniczny IAuthorizationFilter, więc nie mogę używać go asyncdo końca
Catalin
3
@Noseratio to nierealny cel. Istnieje zbyt wiele bibliotek z kodem asynchronicznym i synchronicznym, a także sytuacje, w których użycie tylko jednego modelu nie jest możliwe. Na przykład filtry akcji MVC nie obsługują kodu asynchronicznego.
Justin Skiles
9
@Noserato, pytanie dotyczy wywołania metody asynchronicznej z synchronicznego. Czasami nie możesz zmienić implementowanego interfejsu API. Załóżmy, że implementujesz interfejs synchroniczny z jakiegoś frameworka „A” innej firmy (nie możesz przepisać frameworka na sposób asynchroniczny), ale biblioteka „B” innej firmy, której próbujesz użyć w swojej implementacji, ma jedynie asynchroniczność. Powstały również produkt jest biblioteką i może być używany w dowolnym miejscu, w tym ASP.NET itp.
dimzon
19

Microsoft Identity ma metody rozszerzeń, które wywołują metody asynchroniczne synchronicznie. Na przykład istnieje metoda GenerateUserIdentityAsync () i równa CreateIdentity ()

Jeśli spojrzysz na UserManagerExtensions.CreateIdentity (), wygląda to tak:

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

Teraz pozwala zobaczyć, co robi 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();
    }

To jest opakowanie dla metody asynchronicznej. I proszę nie czytaj danych z Rezultatu - potencjalnie zablokuje Twój kod w ASP.

Jest jeszcze inny sposób - co jest dla mnie podejrzane, ale można to również rozważyć

  Result r = null;

            YourAsyncMethod()
                .ContinueWith(t =>
                {
                    r = t.Result;
                })
                .Wait();
Witalij Markitanow
źródło
3
Co uważasz za problem z drugim sugerowanym przez Ciebie sposobem?
David Clarke,
@DavidClarke prawdopodobnie problem bezpieczeństwa wątków związany z dostępem do nieulotnej zmiennej z wielu wątków bez blokady.
Theodor Zoulias,
9

Aby zapobiec zakleszczeniom, zawsze staram się używać, Task.Run()gdy muszę synchronicznie wywoływać metodę asynchroniczną, o której wspomina @Heinzi.

Jednak metoda musi zostać zmodyfikowana, jeśli metoda asynchroniczna wykorzystuje parametry. Na przykład Task.Run(GenerateCodeAsync("test")).Resultpodaje błąd:

Argument 1: nie można przekonwertować z „ System.Threading.Tasks.Task<string>” na „System.Action”

Zamiast tego można to tak nazwać:

string code = Task.Run(() => GenerateCodeAsync("test")).Result;
Ogglas
źródło
6

Większość odpowiedzi w tym wątku jest albo złożona, albo spowoduje impas.

Poniższa metoda jest prosta i pozwoli uniknąć impasu, ponieważ czekamy na zakończenie zadania i dopiero wtedy otrzymujemy jego wynik

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

Ponadto znajduje się odniesienie do artykułu MSDN, który mówi o dokładnie tej samej rzeczy - https://blogs.msdn.microsoft.com/jpsanders/2017/08/28/asp-net-do-not-use-task-result- w głównym kontekście /

kamalpreet
źródło
1

Wolę podejście nieblokujące:

            Dim aw1=GenerateCodeAsync().GetAwaiter()
            While Not aw1.IsCompleted
                Application.DoEvents()
            End While
Zibri
źródło
0

Cóż, używam tego podejścia:

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

        return task.Result;
    }
Jiří Herník
źródło
-1

Innym sposobem może być czekanie na zakończenie zadania:

var t = GenerateCodeService.GenerateCodeAsync();
Task.WhenAll(t);
string code = t.Result;
frablaser
źródło
1
To po prostu źle. WhenAll zwraca również zadanie, na które nie czekasz.
Robert Schmidt,
-1

EDYTOWAĆ:

Task ma metodę Wait, Task.Wait (), która czeka na „obietnicę” do rozwiązania, a następnie kontynuuje, czyniąc ją synchroniczną. przykład:


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

String mySyncMethod() {

    return MyAsyncMethod().Wait();
}
Avi Tshuva
źródło
3
Prosimy o rozwinięcie odpowiedzi. Jak to jest używane? Jak konkretnie pomaga odpowiedzieć na pytanie?
Scratte
-2

Jeśli masz metodę asynchroniczną o nazwie „ RefreshList ”, możesz wywołać tę metodę asynchroniczną z metody innej niż asynchronicznej, jak poniżej.

Task.Run(async () => { await RefreshList(); });
dush88c
źródło