2016-08-23 3 views
5

nehme ich eine Bibliothek geschrieben haben, die auf async Methoden beruht:Ein Fall, wenn ConfigureAwait (false) statt Deadlock einen Fehler verursacht

namespace MyLibrary1 
{ 
    public class ClassFromMyLibrary1 
    { 
     public async Task<string> MethodFromMyLibrary1(string key, Func<string, Task<string>> actionToProcessNewValue) 
     { 
      var remoteValue = await GetValueByKey(key).ConfigureAwait(false); 

      //do some transformations of the value 
      var newValue = string.Format("Remote-{0}", remoteValue); 

      var processedValue = await actionToProcessNewValue(newValue).ConfigureAwait(false); 

      return string.Format("Processed-{0}", processedValue); 
     } 

     private async Task<string> GetValueByKey(string key) 
     { 
      //simulate time-consuming operation 
      await Task.Delay(500).ConfigureAwait(false); 

      return string.Format("ValueFromRemoteLocationBy{0}", key); 
     } 
    } 
} 

ich die Empfehlungen gefolgt ConfigureAwait(false) der Verwendung (wie in this post) überall in meine Bibliothek. Dann benutze ich es in synchron Weg von meinem Test-App und einen Fehler erhalten:

namespace WpfApplication1 
{ 
    /// <summary> 
    /// Interaction logic for MainWindow.xaml 
    /// </summary> 
    public partial class MainWindow : Window 
    { 
     public MainWindow() 
     { 
      InitializeComponent(); 
     } 

     private void Button1_OnClick(object sender, RoutedEventArgs e) 
     { 
      try 
      { 
       var c = new ClassFromMyLibrary1(); 

       var v1 = c.MethodFromMyLibrary1("test1", ActionToProcessNewValue).Result; 

       Label2.Content = v1; 
      } 
      catch (Exception ex) 
      { 
       System.Diagnostics.Trace.TraceError("{0}", ex); 
       throw; 
      } 
     } 

     private Task<string> ActionToProcessNewValue(string s) 
     { 
      Label1.Content = s; 
      return Task.FromResult(string.Format("test2{0}", s)); 
     } 
    } 
} 

Der Fehler ist:

WpfApplication1.vshost.exe Error: 0 : System.InvalidOperationException: The calling thread cannot access this object because a different thread owns it. at System.Windows.Threading.Dispatcher.VerifyAccess() at System.Windows.DependencyObject.SetValue(DependencyProperty dp, Object value) at System.Windows.Controls.ContentControl.set_Content(Object value) at WpfApplication1.MainWindow.ActionToProcessNewValue(String s) in C:\dev\tests\4\WpfApplication1\WpfApplication1\MainWindow.xaml.cs:line 56 at MyLibrary1.ClassFromMyLibrary1.d__0.MoveNext() in C:\dev\tests\4\WpfApplication1\WpfApplication1\MainWindow.xaml.cs:line 77 --- End of stack trace from previous location where exception was thrown --- at System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess(Task task) at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task) at System.Runtime.CompilerServices.TaskAwaiter`1.GetResult() at WpfApplication1.MainWindow.d__1.MoveNext() in C:\dev\tests\4\WpfApplication1\WpfApplication1\MainWindow.xaml.cs:line 39 Exception thrown: 'System.InvalidOperationException' in WpfApplication1.exe

Offensichtlich ist der Fehler tritt auf, weil die awaiters in meiner Bibliothek aktuellen Kontext WPF verwerfen .

Auf der anderen Seite, nach dem Entfernen der ConfigureAwait(false) überall in der Bibliothek bekomme ich offensichtlich eine Deadlock statt.

There is more detailed example of code was einige Einschränkungen erklärt, mit denen ich mich befassen muss.

Also, wie kann ich dieses Problem angehen? Was ist der beste Ansatz hier? Muss ich immer noch die Best Practice bezüglich ConfigureAwait befolgen?

PS, im realen Szenario habe ich viele Klassen und Methoden daher Tonnen solcher async Anrufe in meiner Bibliothek. Es ist nahezu unmöglich herauszufinden, ob ein bestimmter Async-Aufruf Kontext erfordert oder nicht (siehe Kommentare zur @Alisson-Antwort), um ihn zu beheben. Die Leistung interessiert mich zumindest an dieser Stelle nicht. Ich suche nach einem allgemeinen Ansatz, um dieses Problem anzugehen.

+1

Sie müssen 'ConfigureAwait (false)' nicht von überall löschen, der einzige Ort, den Sie entfernen müssen, ist 'erwarten GetValueByKey (key) .ConfigureAwait (false);' und es wird funktionieren. Siehe [Allisions Antwort] (http://stackoverflow.com/a/39106835/80274) für eine vollständige Erklärung warum. –

+1

Ihre Anforderungen sind nachweislich unmöglich zu erfüllen. Sie möchten, dass Ihr UIhread dort nichts tut, und dass er nichts tun kann, bis Ihre Operation abgeschlossen ist. Gleichzeitig müssen Sie dieselbe Operation ausführen, um Code im UI-Thread auszuführen, bevor er abgeschlossen werden kann. Ihre Anforderungen * Mandat * eine Sackgasse. Eine dieser beiden Anforderungen muss entfernt werden, damit das Problem lösbar ist. – Servy

+0

Sie haben Recht. Ich habe den Client-Code vereinfacht, um den Aufruf _synchronous_ zu demonstrieren. In realem Code kann ich nicht ändern, wie die Bibliotheksmethode aufgerufen wird, weil sie von der _synchronous_-Methode aufgerufen wird, die von einer anderen _synchronous_-Methode usw. aufgerufen wird. Diese Kette kommt schließlich vom GUI-Event-Handler. – neleus

Antwort

3

Normalerweise wird eine Bibliothek dokumentieren, ob ein Callback garantiert in demselben Thread ist, der sie aufgerufen hat. Wenn dies nicht dokumentiert ist, ist die sicherste Option anzunehmen, dass dies nicht der Fall ist. Ihr Codebeispiel (und die dritte Partei, mit der Sie arbeiten, aus dem, was ich anhand Ihrer Kommentare feststellen kann) fallen unter die Kategorie "Nicht garantiert". In dieser Situation müssen Sie nur überprüfen, ob Sie innerhalb der Callback-Methode eine Invoke tun müssen, und Sie können Dispatcher.CheckAccess() aufrufen und es wird false zurückgeben, wenn Sie vor der Verwendung des Steuerelements aufrufen müssen.

private async Task<string> ActionToProcessNewValue(string s) 
{ 
    //I like to put the work in a delegate so you don't need to type 
    // the same code for both if checks 
    Action work =() => Label1.Content = s; 
    if(Label1.Dispatcher.CheckAccess()) 
    { 
     work(); 
    } 
    else 
    { 
     var operation = Label1.Dispatcher.InvokeAsync(work, DispatcherPriority.Send); 

     //We likely don't need .ConfigureAwait(false) because we just proved 
     // we are not on the UI thread in the if check. 
     await operation.Task.ConfigureAwait(false); 
    } 

    return string.Format("test2{0}", s); 
} 

Hier ist eine alternative Version mit einem synchronen Rückruf anstelle eines asynchronen Rückrufs.

private string ActionToProcessNewValue(string s) 
{ 
    Action work =() => Label1.Content = s; 
    if(Label1.Dispatcher.CheckAccess()) 
    { 
     work(); 
    } 
    else 
    { 
     Label1.Dispatcher.Invoke(work, DispatcherPriority.Send); 
    } 

    return string.Format("test2{0}", s); 
} 

Hier ist eine andere Version, wenn Sie den Wert von Label1.Content statt Zuweisung es, dies auch Asynchron/await innerhalb der Callback verwenden, muss nicht bekommen wollte.

private Task<string> ActionToProcessNewValue(string s) 
{ 
    Func<string> work =() => Label1.Content.ToString(); 
    if(Label1.Dispatcher.CheckAccess()) 
    { 
     return Task.FromResult(work()); 
    } 
    else 
    { 
     return Label1.Dispatcher.InvokeAsync(work, DispatcherPriority.Send).Task; 
    } 
} 

WICHTIGER HINWEIS: alle diese Methoden wird Ihr Programm verursachen Deadlock, wenn Sie nicht bekommen, der .Result in der Schaltfläche Click-Handler zu befreien, die Dispatcher.Invoke oder die Dispatcher.InvokeAsync in den Rückruf wird nie während starten Es wartet auf .Result, um zurückzukehren, und .Result wird nie zurückkehren, während es darauf wartet, dass der Rückruf zurückgegeben wird. Sie müssen den Click-Handler zu async void ändern und einen await anstelle des .Result tun.

+0

Die Lösung besteht also darin, den Clientcode zu ändern, um Aktualisierungen im richtigen Kontext vorzunehmen. Ich nahm das an. Was ist nicht klar, muss ich alle "ConfigureAwait (false)" aus meiner Bibliothek entfernen? – neleus

+0

Nein, behalten Sie sie bei, dokumentieren Sie jedoch in der Bibliothek, dass die Callbacks bei der Ausführung möglicherweise nicht im aufrufenden Thread enthalten sind. Dies warnt die Benutzer der Bibliothek, dass sie weder lang laufende Arbeit im Rückruf ausführen noch Annahmen treffen sollten, dass es ohne Überprüfung sicher ist, auf die Benutzeroberfläche zuzugreifen. –

+0

Sobald Ihre Bibliothek es nicht mehr garantiert, denke ich, es ist nicht sicher, ein Label in Ihrem Callback zu aktualisieren, wie Sie es tun, aber es ist sicher, es nach dem Aufruf in Ihrem Button Click Event Handler zu aktualisieren (da Sie gerade Entferne das 'ConfigureAwait' dort. – Alisson

1

Meiner Meinung nach sollten Sie Ihre Bibliotheks-API neu entwerfen, um eine Callback-basierte API nicht mit einer Task-basierten API zu mischen. Zumindest in Ihrem Beispielcode gibt es keinen zwingenden Grund, dies zu tun, und Sie haben einen Grund, dies nicht zu tun - es ist schwierig, den Kontext zu steuern, in dem Ihr Callback läuft.

würde ich Ihrer Bibliothek API wie so sein ändern:

namespace MyLibrary1 
{ 
    public class ClassFromMyLibrary1 
    { 
     public async Task<string> MethodFromMyLibrary1(string key) 
     { 
      var remoteValue = await GetValueByKey(key).ConfigureAwait(false); 
      return remoteValue; 
     } 

     public string TransformProcessedValue(string processedValue) 
     { 
      return string.Format("Processed-{0}", processedValue); 
     } 

     private async Task<string> GetValueByKey(string key) 
     { 
      //simulate time-consuming operation 
      await Task.Delay(500).ConfigureAwait(false); 

      return string.Format("ValueFromRemoteLocationBy{0}", key); 
     } 
    } 
} 

Und es nennen wie so:

private async void Button1_OnClick(object sender, RoutedEventArgs e) 
    { 
     try 
     { 
      var c = new ClassFromMyLibrary1(); 

      var v1 = await c.MethodFromMyLibrary1("test1"); 
      var v2 = await ActionToProcessNewValue(v1); 
      var v3 = c.TransformProcessedValue(v2); 

      Label2.Content = v3; 
     } 
     catch (Exception ex) 
     { 
      System.Diagnostics.Trace.TraceError("{0}", ex); 
      throw; 
     } 
    } 

    private Task<string> ActionToProcessNewValue(string s) 
    { 
     Label1.Content = s; 
     return Task.FromResult(string.Format("test2{0}", s)); 
    } 
+0

Was passiert, wenn 'ActionToProcessNewValue' in einer Schnittstelle deklariert wird und' ClassFromMyLibrary1' diese im Konstruktor akzeptiert? Wie auch immer, der Code kann viel komplizierter sein und der Callback kann zu langen Ketten von injizierten Klassen führen (zum Beispiel unter Verwendung von DI). Deshalb ist es möglicherweise nicht möglich, es neu zu gestalten. – neleus

+0

BTW, ich habe die Methode 'Button1_OnClick' korrigiert und synchron gemacht, da dies eine Voraussetzung ist. – neleus

3

Eigentlich sind Sie einen Rückruf in Ihrem ClassFromMyLibrary1 empfangen und Sie können gehe nicht davon aus, was es tun wird (wie ein Label zu aktualisieren). Sie brauchen nicht ConfigureAwait(false) in Ihrer Klassenbibliothek, wie die gleichen Link wie dies eine Erklärung gibt uns zur Verfügung gestellt:

As asynchronous GUI applications grow larger, you might find many small parts of async methods all using the GUI thread as their context. This can cause sluggishness as responsiveness suffers from "thousands of paper cuts".

To mitigate this, await the result of ConfigureAwait whenever you can.

By using ConfigureAwait, you enable a small amount of parallelism: Some asynchronous code can run in parallel with the GUI thread instead of constantly badgering it with bits of work to do.

Jetzt ein nehmen hier lesen:

You should not use ConfigureAwait when you have code after the await in the method that needs the context. For GUI apps, this includes any code that manipulates GUI elements, writes data-bound properties or depends on a GUI-specific type such as Dispatcher/CoreDispatcher.

Sie tun genau das Gegenteil. Sie versuchen, GUI in zwei Punkten zu aktualisieren, ein in Ihrer Callback-Methode, und ein anderer hier:

var c = new ClassFromMyLibrary1(); 

var v1 = c.MethodFromMyLibrary1("test1", ActionToProcessNewValue).Result; 

Label2.Content = v1; // updating GUI... 

Deshalb ConfigureAwait(false) Entfernen Ihr Problem löst. Außerdem können Sie Ihren Button-Klick-Handler asynchron machen und auf Ihren Methodenaufruf ClassFromMyLibrary1 warten.

+0

Sie möchten vielleicht erwähnen, dass Sie '.ConfigureAwait (false)' nicht überall entfernen müssen, um das Problem zu lösen, der einzige Ort, an dem Sie es entfernen müssen, ist 'getValueByKey (key) abwarten.ConfigureAwait (false); 'und es funktioniert, weil Sie nur den Kontext bis zu dem Punkt benötigen, an dem Sie' await actionToProcessNewValue (newValue) .ConfigureAwait (false); 'aufrufen, das' .ConfigureAwait (false) 'kann dort gehalten werden und alle anderen Orte. –

+1

@ScottChamberlain Wenn diese Klassenbibliotheksmethode eine andere aufruft, die eine andere aufruft usw., müssen sie den Kontext nicht selbst erfassen. Was aber, wenn eine der Methoden irgendwann innerhalb dieser Klassenbibliothek eine Callback-Funktion ausführt, die versucht, ein "Label" zu aktualisieren, so wie er es tut? Ich kenne die GUI-Manipulation nach dem ersten Aufruf ohne 'ConfigureAwait' in der Button-Event-Handler funktioniert, aber dieser Rückruf weiß ich nicht sicher. Würde es immer noch funktionieren? Du könntest meine Antwort verbessern, wenn du es wünschst, ich wäre froh. – Alisson

+0

Richtig, die Wurzel des Problems kommt von der Tatsache, dass der Bibliotheksentwickler nicht weiß, ob ein asynchroner Aufruf den Kontext benötigt oder nicht. Beide Aufrufe 'erwarten GetValueByKey (key)' und 'await actionToProcessNewValue (newValue) 'benötigen den Kontext möglicherweise nicht. – neleus

Verwandte Themen