2015-06-18 8 views
5

Ich schrieb vor kurzem den folgenden Code:async/erwarten vs. handgemachte Fortsetzungen: wird ExecuteSynchronously cleverly verwendet?

Task<T> ExecAsync<T>(string connectionString, SqlCommand cmd, Func<SqlCommand, T> resultBuilder, CancellationToken cancellationToken = default(CancellationToken)) 
    { 
     var tcs = new TaskCompletionSource<T>(); 

     SqlConnectionProvider p; 
     try 
     { 
      p = GetProvider(connectionString); 
      Task<IDisposable> openTask = p.AcquireConnectionAsync(cmd, cancellationToken); 
      openTask 
       .ContinueWith(open => 
       { 
        if(open.IsFaulted) tcs.SetException(open.Exception.InnerExceptions); 
        else if(open.IsCanceled) tcs.SetCanceled(); 
        else 
        { 
         var execTask = cmd.ExecuteNonQueryAsync(cancellationToken); 
         execTask.ContinueWith(exec => 
         { 
          if(exec.IsFaulted) tcs.SetException(exec.Exception.InnerExceptions); 
          else if(exec.IsCanceled) tcs.SetCanceled(); 
          else 
          { 
           try 
           { 
            tcs.SetResult(resultBuilder(cmd)); 
           } 
           catch(Exception exc) { tcs.TrySetException(exc); } 
          } 
         }, TaskContinuationOptions.ExecuteSynchronously); 
        } 
       }) 
       .ContinueWith(_ => 
       { 
        if(!openTask.IsFaulted) openTask.Result.Dispose(); 
       }, TaskContinuationOptions.ExecuteSynchronously); 
     } 
     catch(Exception ex) 
     { 
      tcs.SetException(ex); 
     } 
     return tcs.Task; 
    } 

Das wie vorgesehen funktioniert. Der gleiche Code mit Asynchron geschrieben/erwarten ist (natürlich) einfacher:

async Task<T> ExecAsync<T>(string connectionString, SqlCommand cmd, Func<SqlCommand, T> resultBuilder, CancellationToken cancellationToken = default(CancellationToken)) 
{ 
    SqlConnectionProvider p = GetProvider(connectionString); 
    using(IDisposable openTask = await p.AcquireConnectionAsync(cmd, cancellationToken)) 
    { 
     await cmd.ExecuteNonQueryAsync(cancellationToken); 
     return resultBuilder(cmd); 
    } 
} 

Ich hatte einen kurzen Blick auf die erzeugte IL für die 2 Versionen: die Asynchron/await ist größer (keine Überraschung), aber ich habe mich gefragt, wenn der async/erwarten Code-Generator die Tatsache analysiert, dass eine Fortsetzung tatsächlich synchron ist, um TaskContinuationOptions.ExecuteSynchronously zu verwenden, wo es kann ... und ich fand es nicht in dem IL-generierten Code.

Wenn jemand weiß, diese oder haben keine Ahnung davon, würde ich gerne wissen!

+0

Die Async-Version ist eigentlich nicht synchron. Woher hast du diese Idee? – Martijn

+0

@Martijn: Sorry es war nicht klar. Mein Punkt ist, dass, wenn die Fortsetzung ist nur 'if (! OpenTask.IsFaulted) openTask.Result.Dispose();', Ich möchte es direkt auf der "vorherigen" laufenden Aufgabe ausgeführt werden, um jede Scheduling/Kontextwechsel zu vermeiden . – Spi

+0

Vom Dekompilieren von TaskAwaiter Ich denke, das kann nicht mit dem eingebauten Erwarter getan werden. Sie müssen Ihren eigenen Warter schreiben. Wahrscheinlich nicht wert. – usr

Antwort

8

ich mich gefragt, ob der Asynchron/await Codegenerator die Tatsache analysiert, die eine Fortsetzung tatsächlich synchron ist TaskContinuationOptions.ExecuteSynchronously zu verwenden, wo es kann ... und ich scheiterte diesen Code in dem IL zu finden.

Ob await Fortsetzungen - ohne ConfigureAwait(continueOnCapturedContext: false) - ausführen asynchron oder synchron ist abhängig von der Anwesenheit eines Synchronisationskontextes auf dem Faden, den Code ausgeführt wurde, als er den Punkt await getroffen. Wenn SynchronizationContext.Current != null, hängt das weitere Verhalten von der Implementierung SynchronizationContext.Post ab.

Z. B., wenn Sie auf dem Haupt UI-Thread von einer WPF/WinForms-Anwendung sind, wird Ihre Fortsetzung auf dem gleichen Thread ausgeführt werden, aber immer noch asynchron, bei einer späteren Iteration der Nachrichtenschleife. Es wird über SynchronizationContext.Post gebucht werden. Dies ist vorausgesetzt, dass die Antezedens-Aufgabe in einem Thread-Pool-Thread oder in einem anderen Synchronisationskontext (z. B. Why a unique synchronization context for each Dispatcher.BeginInvoke callback?) abgeschlossen wurde.

Wenn das Antecedens-Task auf ein Gewinde mit dem gleichen Synchronisationskontext (beispielsweise ein WinForm UI-Thread) abgeschlossen ist, wird die Fortsetzung awaitsynchron (inlined) ausgeführt werden. SynchronizationContext.Post wird in diesem Fall nicht verwendet.

Bei fehlendem Synchronisationskontext wird eine await Fortsetzung synchron auf demselben Thread ausgeführt, auf dem die Vorgängeraufgabe abgeschlossen wurde.

Dies ist, wie Sie es von Ihrer ContinueWith mit TaskContinuationOptions.ExecuteSynchronously Implementierung unterschiedlich ist, die überhaupt nicht über den Synchronisations Kontext entweder anfänglichen Thread oder Abschlussthread und führt immer die Fortsetzung synchron scheren (es gibt exceptions to this behavior, dennoch).

Sie können ConfigureAwait(continueOnCapturedContext: false) verwenden, um näher an das gewünschte Verhalten heranzukommen, aber seine Semantik unterscheidet sich immer noch von TaskContinuationOptions.ExecuteSynchronously. In der Tat weist es den Scheduler zu nicht eine Fortsetzung auf einem Thread mit einem Synchronisierungskontext ausführen, so dass Sie Situationen auftreten können, in denen ConfigureAwait(false)pushes the continuation to thread pool, während Sie möglicherweise eine synchrone Ausführung erwartet haben.

Auch verbunden: Revisiting Task.ConfigureAwait(continueOnCapturedContext: false).

+1

Das ist die Antwort. Ich habe gerade dieses Verhalten getestet. Die erwartete Fortsetzung kann tatsächlich inline sein. http://pastebin.com/0eem3jDh Das druckt 9, 11, 11. Der Aufruf-Stack zeigt, dass der Timer abgelaufen ist, was dazu führte, dass die Fortsetzung synchron lief, was dazu führte, dass die Warte-Fortsetzung synchron lief. Der entsprechende Code befindet sich in RunOrScheduleAction. – usr

+0

@ usr, tks. Ich habe tatsächlich vergessen, das Inlining-Verhalten zu erwähnen. Auf einem Thread * mit * Synchronisationskontext wird eine 'wartet'-Fortsetzung inline * sein, wenn * die Ante-Aufgabe in einem Thread mit derselben Synchronisation abgeschlossen wurde. Kontext (z. B. ein UI-Thread) und 'ConfigureAwait (false)' wurde nicht verwendet. – Noseratio

+1

Faszinierend. Es ist beängstigend, wie viel Finsternis in Randfällen um den TPL herum liegt, wie Warte- und Synchronisationskontexte. Das Team hat im Namen der Leistung schreckliche Standard-Entscheidungen getroffen. – usr

0

Solche Optimierungen werden auf der Task-Scheduler-Ebene durchgeführt. Der Task-Scheduler hat nicht nur eine große Warteschlange von Aufgaben zu erfüllen; es teilt sie in verschiedene Aufgaben für jeden der Worker-Threads auf, die es hat. Wenn die Arbeit von einem dieser Worker-Threads aus geplant wird (was bei vielen Fortsetzungen häufig passiert), wird sie der Warteschlange für diesen Thread hinzugefügt. Dies stellt sicher, dass bei einer Operation mit einer Reihe von Fortsetzungen diese Kontextwechsel zwischen Threads minimiert werden. Wenn ein Thread keine Arbeit mehr hat, kann er auch Arbeitselemente aus der Warteschlange eines anderen Threads ziehen, sodass alle beschäftigt bleiben können.

Natürlich alles gesagt, keiner der tatsächlichen Aufgaben, die Sie in Ihrem Code warten tatsächlich CPU gebunden Arbeit; Sie sind IO gebundene Arbeit, so dass sie nicht auf Worker-Threads ausgeführt werden, die auch weiterhin verwendet werden könnten, um die Fortsetzung zu behandeln, da ihre Arbeit nicht von zugewiesenen Threads an erster Stelle getan wird.

+1

Richtig :). Die beiden Aufrufe enden hier bei OpenConnectionAsync und ExecuteQueryAsync, die (zumindest in dem aktuellen .Net-Framework, soweit ich weiß) korrekt implementiert sind: Diese I/O-gebundenen Operationen laufen tatsächlich auf I/O-Threads und nicht auf ThreadPool-Threads. ABER, es gibt ein kleines Stück Code, das vor und nach dem Erreichen dieser "echten asynchronen Punkte" ausgeführt werden muss. Meine Idee war, dass "ExecuteSynchronously", wie Stephen Toub es hier erklärt (http://blogs.msdn.com/b/pfxteam/archive/2012/02/07/10265067.aspx), das Queueing vermeidet, auch wenn es so ist im selben Thread ... – Spi

+0

@Spi Sie laufen nicht auf IO-Threads; Sie * werden überhaupt nicht auf Threads ausgeführt *. Sie sind * eigentlich * asynchrone Operationen, eher als synchrone Operationen, bei denen ein Thread dort sitzt und darauf wartet, dass er beendet wird. – Servy

+0

Die Vervollständigung läuft auf Pool/IO-Threads (egal welche) und es gibt Warteschlangen Overhead ohne ExecuteSynchronously. – usr