20

Ich aktualisiere gerade eine Bibliothek mit einer API-Oberfläche, die in .NET 3.5 erstellt wurde. Daher sind alle Methoden synchron. Ich kann die API nicht ändern (d. H. Rückgabewerte in Task umwandeln), da dies eine Änderung aller Aufrufer erfordern würde. Somit bleibt mir nichts anderes übrig, als asynchrone Methoden synchron aufzurufen. Dies ist im Zusammenhang mit ASP.NET 4, ASP.NET Core und .NET/.NET Core-Konsolenanwendungen.Asynchrone Methoden aus nicht asynchronem Code aufrufen

Ich war vielleicht nicht klar genug - die Situation ist, dass ich bestehenden Code habe, der nicht async bewusst ist, und ich möchte neue Bibliotheken wie System.Net.Http und das AWS SDK verwenden, die nur asynchrone Methoden unterstützen. Also muss ich die Lücke überbrücken und in der Lage sein, Code zu haben, der synchron aufgerufen werden kann, aber dann anderswo asynchrone Methoden aufrufen kann.

Ich habe viel gelesen, und es gibt eine Reihe von Malen, die gefragt und beantwortet wurden.

Calling async method from non async method

Synchronously waiting for an async operation, and why does Wait() freeze the program here

Calling an async method from a synchronous method

How would I run an async Task<T> method synchronously?

Calling async method synchronously

How to call asynchronous method from synchronous method in C#?

Das Problem ist, dass die meisten Antworten unterschiedlich sind! Der häufigste Ansatz, den ich gesehen habe, ist die Verwendung .Result, aber das kann Deadlocks sein. Ich habe alles Folgende versucht, und sie funktionieren, aber ich bin nicht sicher, welcher der beste Ansatz ist, um Deadlocks zu vermeiden, eine gute Leistung zu haben und schön mit der Laufzeit zu spielen (in Bezug auf die Einhaltung von Task Schedulern, Task Creation Optionen, etc). Gibt es eine definitive Antwort? Was ist der beste Ansatz? wie man am besten Aufruf asynchroner Methoden in einer synchronen Weise

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; 
    } 
+4

Die Antwort ist, tun Sie es nicht. Sie fügen neue asynchrone Methoden hinzu und behalten die alten synchronen für die älteren Aufrufer bei. –

+3

Das scheint ein bisschen hart und tötet wirklich die Fähigkeit, neuen Code zu verwenden. Zum Beispiel hat die neue Version des AWS SDK keine Nicht-Async-Methoden. Gleiches gilt für eine Reihe anderer Bibliotheken von Drittanbietern. Also, wenn du die Welt nicht umschreibst, kannst du keine davon benutzen? –

+0

Option 8: Vielleicht könnte die [TaskCompletionSource] (https://msdn.microsoft.com/en-us/library/dd449174 (v = vs.110) .aspx) eine Option sein? – OrdinaryOrange

Antwort

32

Also ich bin mit links.

Zunächst ist dies eine gute Sache zu tun. Ich sage das, weil es auf Stack Overflow üblich ist, dies als eine Tat des Teufels als eine pauschale Aussage ohne Rücksicht auf den konkreten Fall aufzuzeigen.

Es ist nicht erforderlich, dass der gesamte Weg für die Richtigkeit async ist. Das Blockieren von etwas Asynchronem, um es zu synchronisieren, hat einen Leistungsaufwand, der wichtig sein kann oder völlig irrelevant sein kann. Es kommt auf den konkreten Fall an.

Deadlocks kommen von zwei Threads, die versuchen, gleichzeitig in den gleichen single-threaded Synchronisationskontext zu gelangen. Jede Technik, die dies zuverlässig vermeidet, vermeidet Deadlocks, die durch Blockieren verursacht werden.

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

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

.GetAwaiter().GetResult() unterscheidet sich von Result/Wait() darin, dass es das await Ausnahmeausbreitungsverhalten nachahmt. Sie müssen entscheiden, ob Sie das wollen oder nicht. (Recherchiere also, was dieses Verhalten ist; du musst es hier nicht wiederholen.)

Darüber hinaus haben alle diese Ansätze eine ähnliche Leistung. Sie ordnen ein OS-Ereignis auf die eine oder andere Weise zu und blockieren es. Das ist der teure Teil. Ich weiß nicht, welcher Ansatz am günstigsten ist.

Ich persönlich mag die Task.Run(() => DoSomethingAsync()).Wait(); Muster, weil es kategorisch Deadlocks vermeidet, ist einfach und versteckt nicht einige Ausnahmen, die GetResult() verstecken könnte. Aber Sie können auch GetResult() mit diesem verwenden.

+2

Danke, das macht sehr viel Sinn. –

+0

mehr/cross-ref http://StackOverflow.com/A/22629216/56145 –

10

Ich aktualisiere gerade eine Bibliothek mit einer API-Oberfläche, die in .NET 3.5 erstellt wurde. Daher sind alle Methoden synchron. Ich kann die API nicht ändern (d. H. Rückgabewerte in Task umwandeln), da dies eine Änderung aller Aufrufer erfordern würde. Somit bleibt mir nichts anderes übrig, als asynchrone Methoden 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.

Was ich empfehle ist, dass Sie die alten synchronen APIs beibehalten und dann asynchrone APIs neben ihnen einfügen. Sie können dies mit der "boolean argument hack" as described in my MSDN article on Brownfield Async tun.

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

  1. ConfigureAwait macht nur Sinn, wenn es ein await ist; sonst tut es nichts.
  2. Result wickelt Ausnahmen in einem AggregateException; Wenn Sie blockieren müssen, verwenden Sie stattdessen GetAwaiter().GetResult().
  3. Task.Run wird seinen Code auf einem Thread-Pool-Thread ausführen (natürlich). Dies ist in Ordnung nur wenn der Code kann auf einem Thread-Pool-Thread ausführen.
  4. RunSynchronously ist eine erweiterte API, die in extrem seltenen Situationen bei der dynamischen aufgabenbasierten Parallelität verwendet wird. Du bist überhaupt nicht in diesem Szenario.
  5. Task.WaitAll mit einer einzigen Aufgabe ist das gleiche wie nur Wait().
  6. async() => await x ist nur eine weniger effiziente Art zu sagen () => x.
  7. Die Blockierung einer Task wurde vom aktuellen Thread can cause deadlocks gestartet.

Hier ist der Zusammenbruch:

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

Statt jeder dieser Ansätze, da Sie bestehenden haben, Synchroncode arbeiten, können Sie es neben dem neueren natürlich asynchronen Code verwenden sollten.Zum Beispiel verwendet, wenn Ihre vorhandenen Code WebClient:

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

und Sie wollen ein asynchrones API hinzuzufügen, dann würde ich es tun, wie folgt:

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 must Verwendung HttpClient aus 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); 

Mit diesem Ansatz würde die Logik in die Core Methoden gehen, was sein kann, synchron oder asynchron laufen (wie durch den Parameter sync festgelegt). Wenn synctrue ist, müssen die Kernmethoden eine bereits abgeschlossene Aufgabe zurückgeben. Verwenden Sie für die Implementierung synchrone APIs, um synchron zu arbeiten, und verwenden Sie asynchrone APIs für die asynchrone Ausführung.

Schließlich empfehle ich, die synchronen APIs abzulehnen.

+0

Können Sie mehr den Artikel sechs erklären? –

+0

@EmersonSoares: Wie ich in meinem [asynchronen Intro] (http://blog.stephencleary.com/2012/02/async-and-await.html) erkläre, können Sie das Ergebnis einer Methode "abwarten", weil es zurückkehrt "Aufgabe", nicht weil sie "asynchron" ist. Dies bedeutet, dass Sie [für triviale Methoden die Schlüsselworte löschen können] (http://blog.stephencleary.com/2016/12/eliding-async-wait.html). –

+0

Ich konfrontiert vor kurzem mit dem gleichen Problem in asp.net mvc 5 + EF6. Ich habe TaskFactory verwendet, da diese Antwort https://Stackoverflow.com/a/25097498/1683040 andeutet, und es hat den Trick für mich :), aber nicht sicher für andere Szenarien. – LeonardoX

Verwandte Themen