2016-05-04 3 views
1

Ich weiß nicht, ob ich etwas falsch mache oder ich habe einen Fehler in der Async-Bibliothek gefunden, aber ich habe ein Problem beim Ausführen von Async-Code nach I gesehen kehrte mit continueWith() in den synchronisierten Kontext zurück.ConfigureAwait (False) ändert den Kontext nicht nach ContinueWith()

UPDATE: Der Code läuft jetzt

using System; 
using System.ComponentModel; 
using System.Net.Http; 
using System.Threading.Tasks; 
using System.Windows.Forms; 

namespace WindowsFormsApplication1 
{ 
internal static class Program 
{ 
    [STAThread] 
    private static void Main() 
    { 
     Application.EnableVisualStyles(); 
     Application.SetCompatibleTextRenderingDefault(false); 
     Application.Run(new Form1()); 
    } 
} 

public partial class Form1 : Form 
{ 
    public Form1() 
    { 
     InitializeComponent(); 
     MainFrameController controller = new MainFrameController(this); 
     //First async call without continueWith 
     controller.DoWork(); 

     //Second async call with continueWith 
     controller.DoAsyncWork(); 
    } 

    public void Callback(Task<HttpResponseMessage> task) 
    { 
     Console.Write(task.Result); //IT WORKS 

     MainFrameController controller = 
      new MainFrameController(this); 
     //third async call 
     controller.DoWork(); //IT WILL DEADLOCK, since ConfigureAwait(false) in HttpClient DOESN'T change context 
    } 
} 


internal class MainFrameController 
{ 
    private readonly Form1 form; 

    public MainFrameController(Form1 form) 
    { 
     this.form = form; 
    } 

    public void DoAsyncWork() 
    { 
     Task<HttpResponseMessage> task = Task<HttpResponseMessage>.Factory.StartNew(() => DoWork()); 
     CallbackWithAsyncResult(task); 
    } 

    private void CallbackWithAsyncResult(Task<HttpResponseMessage> asyncPrerequisiteCheck) 
    { 
     asyncPrerequisiteCheck.ContinueWith(task => 
      form.Callback(task), 
      TaskScheduler.FromCurrentSynchronizationContext()); 
    } 

    public HttpResponseMessage DoWork() 
    { 
     MyHttpClient myClient = new MyHttpClient(); 
     return myClient.RunAsyncGet().Result; 
    } 
} 

internal class MyHttpClient 
{ 
    public async Task<HttpResponseMessage> RunAsyncGet() 
    { 
     HttpClient client = new HttpClient(); 
     return await client.GetAsync("https://www.google.no").ConfigureAwait(false); 
    } 
} 

partial class Form1 
{ 
    private IContainer components; 

    protected override void Dispose(bool disposing) 
    { 
     if (disposing && (components != null)) 
     { 
      components.Dispose(); 
     } 
     base.Dispose(disposing); 
    } 

    #region Windows Form Designer generated code 
    private void InitializeComponent() 
    { 
     this.components = new System.ComponentModel.Container(); 
     this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font; 
     this.Text = "Form1"; 
    } 

    #endregion 
} 
} 
  • Der Httpclient-Code, der async ist gut läuft das erste Mal.
  • Dann führe ich den zweiten Async-Code und zurück zum UI-Kontext mit ContinueWith, und es funktioniert gut.
  • Ich führe den HttClient-Code erneut, aber es Deadlock, weil dieses Mal ConfigureAwait (false) den Kontext nicht ändert.
+1

Es wäre schön, wenn dies ein tatsächliches * vollständiges * Beispiel wäre, das das Problem demonstriert. Leider ist es nicht, also würden wir raten. Wenn Sie könnten, versuchen Sie bitte ein [mcve] zu erstellen. –

+2

Hallo, wäre es schön zu wissen, was Sie wirklich erreichen möchten. Da Sie verschiedene Techniken für asynchrone Operationen mischen, ist der Code schwer zu verstehen. Zum Beispiel wird die RunAsyncPost-Methode als DoHttpWork aufgerufen. –

Antwort

6

Das Hauptproblem in Ihrem Code liegt an StartNew und ContinueWith. ContinueWith ist aus den gleichen Gründen gefährlich, StartNew is dangerous, wie ich auf meinem Blog beschreibe.

Zusammengefasst: StartNew und ContinueWith sollten nur verwendet werden, wenn Sie tun dynamic task-based parallelism (was dieser Code nicht).

Die tatsächliche Problem ist, dass HttpClient.GetAsync nicht verwendet (das Äquivalent von) ConfigureAwait(false); Es verwendet ContinueWith mit seinem Standard-Scheduler-Argument (das ist TaskScheduler.Current, nichtTaskScheduler.Default).

näher zu erläutern ...

Der Standard-Scheduler für StartNew und ContinueWith ist nichtTaskScheduler.Default (der Thread-Pool); Es ist TaskScheduler.Current (der aktuelle Taskplaner). Also, in Ihrem Code, DoAsyncWork, wie es derzeit ist, tut nicht führen immer DoWork auf den Thread-Pool.

Das erste Mal DoAsyncWork genannt wird, wird es auf dem UI-Thread aber ohne Strom TaskScheduler aufgerufen werden. In diesem Fall ist TaskScheduler.Current dasselbe wie TaskScheduler.Default, und DoWork wird im Thread-Pool aufgerufen.

Dann ruft CallbackWithAsyncResultForm1.Callback mit einem TaskScheduler auf, der es auf dem UI-Thread ausführt. Wenn Form1.CallbackDoAsyncWork aufruft, wird es auf dem UI-Thread mit ein aktuelles TaskScheduler (der UI-Taskplaner) aufgerufen. In diesem Fall ist TaskScheduler.Current der UI-Taskplaner, und DoAsyncWork endet mit dem Aufruf DoWorkauf dem UI-Thread.

Aus diesem Grund Sie immer ein TaskScheduler beim Aufruf StartNew oder ContinueWith angeben sollten.

Also, das ist ein Problem. Aber es verursacht nicht den Deadlock, den Sie sehen, denn ConfigureAwait(false)sollte ermöglichen diesen Code, um nur die Benutzeroberfläche statt Deadlocks zu blockieren.

Es ist Deadlocking, weil Microsoft den gleichen Fehler gemacht. Auscheckzeile 198 here: (die von GetAsync aufgerufen wird) verwendet ContinueWith ohne Angabe eines Schedulers. Also, es nimmt die TaskScheduler.Current aus Ihrem Code, und wird nie seine Aufgabe abgeschlossen, bis es auf dem Scheduler (d. H. Der UI-Thread) ausgeführt werden kann, was den klassischen Deadlock verursacht.

Es gibt nichts, was Sie tun können, um den Fehler HttpClient.GetAsync zu beheben (offensichtlich). Sie müssen nur umgehen, und der einfachste Weg, dies zu tun ist, um eine TaskScheduler.Current zu vermeiden. Immer wenn du kannst.

Hier einige allgemeine Richtlinien für die asynchrone Code:

  • Nicht immer StartNew verwenden. Verwenden Sie stattdessen Task.Run.
  • Verwenden Sie niemals ContinueWith. Verwenden Sie stattdessen await.
  • Verwenden Sie niemals Result. Verwenden Sie stattdessen await.

Wenn wir das tun nur minimale Änderungen (als Ersatz für StartNew mit Run und ContinueWith mit await), dann DoAsyncWorkimmer führt DoWork auf dem Thread-Pool und die Sackgasse vermieden wird (da await die SynchronizationContext direkt verwendet und keine TaskScheduler):

public void DoAsyncWork() 
{ 
    Task<HttpResponseMessage> task = Task.Run(() => DoWork()); 
    CallbackWithAsyncResult(task); 
} 

private async void CallbackWithAsyncResult(Task<HttpResponseMessage> asyncPrerequisiteCheck) 
{ 
    try 
    { 
     await asyncPrerequisiteCheck; 
    } 
    finally 
    { 
     form.Callback(asyncPrerequisiteCheck); 
    } 
} 

Allerdings ist es immer fraglich, ein Callback-Szenario mit dem Task-basierter Asynchronität haben, weil Aufgaben selbst die pow haben Rückrufe innerhalb von ihnen. Es sieht so aus, als ob Sie eine asynchrone Initialisierung durchführen möchten. Ich habe einen Blogeintrag auf asynchronous construction, der ein paar mögliche Ansätze zeigt.

Selbst etwas wirklich grundlegende wie dies wäre ein besseres Design als Rückrufe (wieder, IMO), auch wenn es async void für die Initialisierung verwendet:

public partial class Form1 : Form 
{ 
    public Form1() 
    { 
     InitializeComponent(); 
     MainFrameController controller = new MainFrameController(); 
     controller.DoWork(); 

     Callback(controller.DoAsyncWork()); 
    } 

    private async void Callback(Task<HttpResponseMessage> task) 
    { 
     await task; 
     Console.Write(task.Result); 

     MainFrameController controller = new MainFrameController(); 
     controller.DoWork(); 
    } 
} 

internal class MainFrameController 
{ 
    public Task<HttpResponseMessage> DoAsyncWork() 
    { 
     return Task.Run(() => DoWork()); 
    } 

    public HttpResponseMessage DoWork() 
    { 
     MyHttpClient myClient = new MyHttpClient(); 
     var task = myClient.RunAsyncGet(); 
     return task.Result; 
    } 
} 

Natürlich gibt es andere Design-Probleme hier: nämlich DoWork blockiert bei einer natürlich-asynchronen Operation und DoAsyncWork blockiert einen Threadpool-Thread bei einer natürlich asynchronen Operation. Wenn Form1DoAsyncWork aufruft, erwartet es einen Threadpool-Vorgang, der bei einem asynchronen Vorgang blockiert wird. Async-über-sync-über-async, das heißt. Sie können auch von meinem blog series on Task.Run etiquette profitieren.

+1

Da war so viel los, ich habe auf eine klare Analyse gewartet. Der OP-Code (mit seiner Mischung aus asynchronen Mechanismen) scheint "eindeutig" entworfen worden zu sein, um * einen Deadlock zu erzeugen. Es war mir unklar, ob ein "Fix" möglich wäre. Der einzige Punkt, den ich in einer separaten Antwort enthalten könnte, ist, dass sie zu glauben scheinen, dass 'ConfigureAwait' die Dinge auf einen separaten Kontext verschiebt, obwohl es fast das Gegenteil ist -" wenn wir nicht zum ursprünglichen Kontext zurückkehren können, ist das okay " , falls das Warten überhaupt passiert. –

+0

@Damien_The_Unbeliever: Ausgezeichneter Punkt; Ich würde das erwähnen, aber vergessen mit all den Worten in dieser Antwort. :) @op: 'ConfigureAwait (false)' ändert den Kontext * nicht *. Es informiert nur die "erwarten", den Kontext nicht zu erfassen. Insbesondere, wenn die Aufgabe bereits abgeschlossen ist, wird ein 'warten ... ConfigureAwait (false) '* nicht * in einen Thread-Pool-Thread verschoben. –

+0

Danke, das ist, was ich gesucht habe, ich war nicht daran interessiert, diesen Code zu reparieren, es war nur ein Beispiel. Ich wollte verstehen, warum es festgefahren war. Und natürlich lernen Sie besser die Verwendung der Async-Tools :) –

0

Verwenden Sie nicht .Result. Wenn Sie überhaupt einen Code haben, der async/await verwendet, vergessen Sie einfach vollständig, dass es überhaupt existiert. Selbst wenn wir es heute schaffen, wird das, was Sie versuchen, so unglaublich spröde sein, dass es morgen nicht unbedingt funktionieren wird.

+0

das .Result läuft unter Aufgabe .Factory.StartNew (() => DoWork()) ;, so sollte es keine Rolle spielen, es wird in einem anderen Kontext sein. also wird die Benutzeroberfläche nicht darauf warten. –

+0

Es geht nicht darum, was ich versuche zu tun, ich versuche zu verstehen, warum der 3. Anruf in die Sackgasse geht. und warum die ConfigureAwait (false) das nicht vermeidet –

+1

@ Max.Moraga - Ich bin mir nicht sicher, was Sie "ConfigureAwait" tun, aber Sie sollten wissen, dass seine einzigen spürbaren Effekte innerhalb der gleichen Methode sind, wie es heißt. Daher ist das Aufrufen von 'ConfigureAwait' kurz vor dem Beenden der Methode (meistens) sinnlos. –

Verwandte Themen