Aufrufen von asynchronen Methoden aus nicht asynchronem Code

75

Ich bin dabei, eine Bibliothek mit einer API-Oberfläche zu aktualisieren, die in .NET 3.5 erstellt wurde. Infolgedessen sind alle Methoden synchron. Ich kann die API nicht ändern (dh Rückgabewerte in Task konvertieren), da dies die Änderung aller Aufrufer erfordern würde. Ich habe also die Möglichkeit, asynchrone Methoden am besten synchron aufzurufen. Dies steht im Zusammenhang mit ASP.NET 4-, ASP.NET Core- und .NET / .NET Core-Konsolenanwendungen.

Ich war möglicherweise nicht klar genug - die Situation ist, dass ich vorhandenen Code habe, der nicht asynchron ist, und ich möchte neue Bibliotheken wie System.Net.Http und das AWS SDK verwenden, die nur asynchrone Methoden unterstützen. Ich muss also die Lücke schließen und in der Lage sein, Code zu haben, der synchron aufgerufen werden kann, aber dann an anderer Stelle asynchrone Methoden aufrufen kann.

Ich habe viel gelesen und es wurde mehrmals gefragt und beantwortet.

Aufrufen der asynchronen Methode von der nicht asynchronen Methode

Synchrones Warten auf eine asynchrone Operation und warum friert Wait () das Programm hier ein

Aufrufen einer asynchronen Methode von einer synchronen Methode

Wie würde ich eine asynchrone Task <T> -Methode synchron ausführen?

Asynchrone Methode synchron aufrufen

Wie rufe ich eine asynchrone Methode von einer synchronen Methode in C # auf?

Das Problem ist, dass die meisten Antworten unterschiedlich sind! Der häufigste Ansatz, den ich gesehen habe, ist die Verwendung von .Result, aber dies kann zum Stillstand kommen. Ich habe alles Folgende ausprobiert und sie funktionieren, aber ich bin mir nicht sicher, welcher Ansatz der beste ist, um Deadlocks zu vermeiden, eine gute Leistung zu erzielen und gut mit der Laufzeit zu spielen (in Bezug auf die Einhaltung von Aufgabenplanern, Optionen zur Aufgabenerstellung usw.) ). Gibt es eine endgültige Antwort? Was ist der beste Ansatz?

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;
    }
Erick T.
quelle
5
Die Antwort ist, dass Sie es nicht tun. Sie fügen neue Methoden hinzu, die asynchron sind, und behalten die alten synchronen Methoden für die älteren Anrufer bei.
Scott Chamberlain
14
Das scheint ein wenig hart zu sein und beeinträchtigt wirklich die Fähigkeit, neuen Code zu verwenden. Beispielsweise verfügt die neue Version des AWS SDK nicht über nicht asynchrone Methoden. Gleiches gilt für eine Reihe anderer Bibliotheken von Drittanbietern. Wenn Sie also die Welt nicht neu schreiben, können Sie keine davon verwenden?
Erick T
Option 8: Vielleicht könnte die TaskCompletionSource eine Option sein?
OrdinaryOrange
Die Result-Eigenschaft verursacht nur Deadlocks, wenn sie aufgerufen wird, während Task noch ausgeführt wird. Ich bin nicht 100% sicher, aber Sie sollten sicher sein, wenn Sie richtig warten, bis die Aufgabe endet, wie in Ansatz 7.
infiniteRefactor
1
Das Zeigen eines Beispiels, warum Sie keine synchrone API aus asynchronem Code verwenden können, könnte helfen, eine bessere Antwort als auf von @scott
Alexei Levenkov

Antworten:

87

Ich habe also die Möglichkeit, asynchrone Methoden am besten synchron aufzurufen.

Erstens ist dies in Ordnung. Ich sage dies, weil es bei Stack Overflow üblich ist, dies als eine Tat des Teufels als pauschale Aussage ohne Rücksicht auf den konkreten Fall herauszustellen.

Es ist nicht erforderlich, dass Sie für die Richtigkeit vollständig asynchron sind . Das Blockieren von etwas Asynchronem, um es zu synchronisieren, hat Leistungskosten, die wichtig oder völlig irrelevant sein können. Es kommt auf den konkreten Fall an.

Deadlocks kommen von zwei Threads, die gleichzeitig versuchen, in denselben Single-Thread-Synchronisationskontext zu gelangen. Jede Technik, die dies vermeidet, vermeidet zuverlässig Deadlocks, die durch Blockieren verursacht werden.

Hier sind alle Ihre Anrufe .ConfigureAwait(false)sinnlos, weil Sie nicht warten.

RunSynchronously ist ungültig, da nicht alle Aufgaben auf diese Weise verarbeitet werden können.

.GetAwaiter().GetResult()unterscheidet sich davon Result/Wait()darin, dass es das awaitVerhalten der Ausnahmeausbreitung nachahmt . Sie müssen entscheiden, ob Sie das wollen oder nicht. (Erforschen Sie also, was dieses Verhalten ist; Sie müssen es hier nicht wiederholen.)

Außerdem weisen alle diese Ansätze eine ähnliche Leistung auf. Sie weisen ein Betriebssystemereignis auf die eine oder andere Weise zu und blockieren es. Das ist der teure Teil. Ich weiß nicht, welcher Ansatz absolut am billigsten ist.

Ich persönlich mag das Task.Run(() => DoSomethingAsync()).Wait();Muster, weil es Deadlocks kategorisch vermeidet, einfach ist und einige Ausnahmen, die sich GetResult()möglicherweise verbergen, nicht verbirgt. Aber Sie können auch damit verwenden GetResult().

usr
quelle
3
Danke, das macht sehr viel Sinn.
Erick T
40

Ich bin dabei, eine Bibliothek mit einer API-Oberfläche zu aktualisieren, die in .NET 3.5 erstellt wurde. Infolgedessen sind alle Methoden synchron. Ich kann die API nicht ändern (dh Rückgabewerte in Task konvertieren), da dies die Änderung aller Aufrufer erfordern würde. Ich habe also die Möglichkeit, asynchrone Methoden am besten synchron aufzurufen.

Es gibt keinen universellen "besten" Weg, um das Sync-over-Async-Anti-Pattern durchzuführen. Nur eine Vielzahl von Hacks, die jeweils ihre eigenen Nachteile haben.

Ich empfehle, dass Sie die alten synchronen APIs beibehalten und dann neben ihnen asynchrone APIs einführen. Sie können dies mit dem "boolean argument hack" tun, wie in meinem MSDN-Artikel über Brownfield Async beschrieben .

Zunächst eine kurze Erläuterung der Probleme bei jedem Ansatz in Ihrem Beispiel:

  1. ConfigureAwaitmacht nur Sinn, wenn es eine gibt await; sonst macht es nichts.
  2. Resultwird Ausnahmen in ein AggregateException; Wenn Sie blockieren müssen, verwenden Sie GetAwaiter().GetResult()stattdessen.
  3. Task.Runführt seinen Code in einem Thread-Pool-Thread aus (offensichtlich). Das ist in Ordnung , nur wenn der Code kann auf einem Thread - Pool Thread ausgeführt.
  4. RunSynchronouslyist eine erweiterte API, die in äußerst seltenen Situationen bei dynamischer aufgabenbasierter Parallelität verwendet wird. Du bist überhaupt nicht in diesem Szenario.
  5. Task.WaitAllmit einer einzigen Aufgabe ist das gleiche wie gerade Wait().
  6. async () => await xist nur eine weniger effiziente Art zu sagen () => x.
  7. Das Blockieren einer vom aktuellen Thread gestarteten Aufgabe kann zu Deadlocks führen .

Hier ist die Aufschlüsselung:

// 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;

Anstelle eines dieser Ansätze sollten Sie, da Sie über vorhandenen, funktionierenden synchronen Code verfügen , diesen zusammen mit dem neueren natürlich asynchronen Code verwenden. Wenn beispielsweise Ihr vorhandener Code verwendet wird WebClient:

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

und Sie möchten eine asynchrone API hinzufügen, dann würde ich es so machen:

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

oder, wenn Sie müssen verwenden HttpClientaus irgendeinem Grund:

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

Bei diesem Ansatz würde Ihre Logik in die CoreMethoden einfließen, die synchron oder asynchron ausgeführt werden können (wie durch den syncParameter bestimmt). Wenn syncist true, dann sind die Kernmethoden müssen zurückgeben eine bereits erledigte Aufgabe. Verwenden Sie für die Implementierung synchrone APIs, um synchron ausgeführt zu werden, und verwenden Sie asynchrone APIs, um asynchron auszuführen.

Schließlich empfehle ich, die synchronen APIs zu verwerfen.

Stephen Cleary
quelle
Können Sie den sechsten Punkt näher erläutern?
Emerson Soares
@EmersonSoares: Wie ich in meinem asynchronen Intro erläutere , können Sie awaitdas Ergebnis einer Methode verwenden, weil sie zurückkehrt Task, nicht weil sie es ist async. Dies bedeutet, dass Sie für einfache Methoden die Schlüsselwörter löschen können .
Stephen Cleary
Ich hatte vor kurzem mit dem gleichen Problem in asp.net mvc 5 + EF6 konfrontiert. Ich habe TaskFactory verwendet, da diese Antwort auf stackoverflow.com/a/25097498/1683040 hindeutet , und es hat den Trick für mich getan :), aber für andere Szenarien nicht sicher.
LeonardoX
Ich verstehe Ihren Vorschlag mit WebClient. Aber welchen Nutzen hat die Core-Methode bei der Verwendung HttpClient? Wäre es nicht sauberer und effizienter, wenn die synchrone Methode die Methode direkt aufruft GetCoreSyncund das boolesche Argument in diesem Fall ausfällt?
Creepin
@Creepin: Ja, ich glaube schon.
Stephen Cleary
1

Genau das habe ich gerade mit dem AWS S3 SDK durchgemacht. Früher war es synchron und ich habe eine Menge Code darauf aufgebaut, aber jetzt ist es asynchron. Und das ist in Ordnung: Sie haben es geändert, nichts, was man durch Stöhnen gewinnen könnte, mach weiter.
Daher muss ich meine App aktualisieren. Ich kann entweder einen großen Teil meiner App so umgestalten, dass sie asynchron ist, oder die asynchrone S3-API "hacken", um sich wie eine Synchronisierung zu verhalten.
Ich werde mich irgendwann mit dem größeren asynchronen Refactoring befassen - es gibt viele Vorteile -, aber für heute habe ich größere Fische zum Braten, also habe ich mich entschieden, die Synchronisierung zu fälschen.

Ursprünglicher Synchronisationscode war
ListObjectsResponse response = api.ListObjects(request);
und ein wirklich einfaches asynchrones Äquivalent , das für mich funktioniert, ist
Task<ListObjectsV2Response> task = api.ListObjectsV2Async(rq2);
ListObjectsV2Response rsp2 = task.GetAwaiter().GetResult();

Ich verstehe zwar, dass Puristen mich dafür anprangern könnten, aber die Realität ist, dass dies nur eines von vielen dringenden Problemen ist und ich endlich Zeit habe, also muss ich Kompromisse eingehen. Perfekt? Funktioniert? Ja.

Dlchambers
quelle
-3

Sie können die asynchrone Methode von der nicht asynchronen Methode aus aufrufen. Überprüfen Sie den folgenden Code.

     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;
    }
Sam Pandya
quelle