2014-02-15 7 views
11

mit dem folgenden Code ...Wie konvertiere ich dies in eine asynchrone Aufgabe?

static void DoSomething(int id) { 
    Thread.Sleep(50); 
    Console.WriteLine(@"DidSomething({0})", id); 
} 

Ich weiß, dass ich dies zu einer async Aufgabe umwandeln kann wie folgt ...

static async Task DoSomethingAsync(int id) { 
    await Task.Delay(50); 
    Console.WriteLine(@"DidSomethingAsync({0})", id); 
} 

Und das von so tun, wenn ich mehrere Male nenne (Task.WhenAll) wird alles schneller und effizienter als vielleicht mit Parallel.Foreach oder sogar von innerhalb einer Schleife aufrufen.

Aber für eine Minute, lassen Sie uns vortäuschen, dass Task.Delay() nicht existiert, und ich muss tatsächlich Thread.Sleep(); Ich weiß in der Realität, dass dies nicht der Fall ist, aber das ist Konzept-Code und wo die Verzögerung/Sleep ist normalerweise eine IO-Operation, wo es keine Async-Option (wie frühe EF).

Ich habe versucht, die folgenden ...

static async Task DoSomethingAsync2(int id) { 
    await Task.Run(() => { 
     Thread.Sleep(50); 
     Console.WriteLine(@"DidSomethingAsync({0})", id); 
    }); 
} 

Aber, obwohl es ohne Fehler läuft, nach Lucien Wischik dies ist in der Tat eine schlechte Praxis, da sie lediglich Fäden aus dem Pool hochgefahren wird jede Aufgabe abzuschließen (es ist auch langsamer die folgende Konsole-Anwendung - wenn Sie tauschen zwischen DoSomethingAsync und DoSomethingAsync2 nennen Sie einen signifikanten Unterschied in der Zeit sehen können, dass es in Anspruch nimmt) ...

static void Main(string[] args) { 
    MainAsync(args).Wait(); 
} 

static async Task MainAsync(String[] args) { 

    List<Task> tasks = new List<Task>(); 
    for (int i = 1; i <= 1000; i++) 
     tasks.Add(DoSomethingAsync2(i)); // Can replace with any version 
    await Task.WhenAll(tasks); 

} 

ich dann folgendes versucht. ..

static async Task DoSomethingAsync3(int id) { 
    await new Task(() => { 
     Thread.Sleep(50); 
     Console.WriteLine(@"DidSomethingAsync({0})", id); 
    }); 
} 

Wenn Sie dies anstelle des ursprünglichen DoSomethingAsync übertragen, wird der Test nie abgeschlossen, und auf dem Bildschirm wird nichts angezeigt!

Ich habe auch mehrere andere Varianten ausprobiert, die entweder nicht kompilieren oder nicht abgeschlossen werden!

Mit der Einschränkung, dass Sie keine vorhandenen asynchronen Methoden aufrufen können und sowohl die Thread.Sleep als auch die Console.WriteLine in einer asynchronen Task ausführen müssen, wie geht das auf eine Weise, die so effizient wie das Original ist Code?

Das Ziel für diejenigen von Ihnen, die interessiert sind, ist es, mir ein besseres Verständnis zu geben, wie ich meine eigenen asynchronen Methoden erstellen kann, wo ich niemanden anrufe. Trotz vieler Suchen scheint dies der einzige Bereich zu sein, in dem Beispiele wirklich fehlen - während es viele tausend Beispiele für den Aufruf asynchroner Methoden gibt, die andere asynchrone Methoden aufrufen, finde ich keine, die eine existierende void-Methode in eine asynchrone Aufgabe konvertieren Es gibt keinen weiteren Aufruf für eine asynchrone Task außer denen, die die Methode Task.Run (() => {}) verwenden.

+0

Ihr letztes Beispiel wird nie abgeschlossen, weil Sie nie schaffen Sie die Aufgabe 'Starten' nennen. – Lee

+0

@lee: Wenn ich auf neue Aufgabe (() => {}) warte, wird Start() nicht kompiliert, weil Task.Start() void zurückgibt. –

+0

Wenn Sie effizient warten müssen, ohne Threads zu blockieren, können Sie stattdessen [TaskCompletionSource] (http://msdn.microsoft.com/en-us/library/dd449174.aspx) verwenden. – Lee

Antwort

5

Es gibt zwei Arten von Aufgaben: diejenigen, die Code ausführen (z. B. Task.Run und Freunde), und solche, die auf ein externes Ereignis reagieren (z. B. TaskCompletionSource<T> und Freunde).

Was Sie suchen, ist TaskCompletionSource<T>. Es gibt verschiedene "shorthand" -Formulare für allgemeine Situationen, so dass Sie nicht immer TaskCompletionSource<T> direkt verwenden müssen. Zum Beispiel Task.FromResult oder TaskFactory.FromAsync. FromAsync wird am häufigsten verwendet, wenn Sie eine vorhandene *Begin/*End Implementierung Ihrer E/A haben; Andernfalls können Sie TaskCompletionSource<T> direkt verwenden.

Weitere Informationen finden Sie im Abschnitt "I/O-gebundene Aufgaben" unter Implementing the Task-based Asynchronous Pattern.

Der Task Konstruktor ist (leider) ein Übergriff von Task-basierter Parallelität und sollte nicht in asynchronem Code verwendet werden. Es kann nur verwendet werden, um eine code-basierte Aufgabe zu erstellen, keine externe Ereignis-Aufgabe.

Also, angesichts der Einschränkung, dass Sie nicht alle vorhandenen asynchronen Methoden aufrufen können und sowohl die Thread.Sleep und die Console.WriteLine in einer asynchronen Aufgabe ausführen müssen, wie Sie es tun in einer Weise, die ist so effizient wie der ursprüngliche Code?

Ich würde einen Timer irgendeiner Art verwenden und es eine TaskCompletionSource<T> abschließen, wenn der Timer ausgelöst wird. Ich bin fast sicher, dass das die tatsächliche Task.Delay Implementierung sowieso tut.

+0

Danke, aber Ihre Referenz auf den Timer und TaskCompletionSource verfehlt vielleicht den Punkt; Der Thread.Sleep war lediglich ein Beispiel für eine lange laufende Aufgabe, die ich abschließen möchte, wenn ich keinen asynchronen Methodenaufruf habe. Vielleicht hätte ich es für eine bessere Klarheit durch eine lang laufende Schleife ersetzen sollen. –

+1

@MartinRobins: Die Frage ist: "Was macht deine Aufgabe lang?" Wenn es tatsächlich * code, * ausführt, dann ist es die richtige Lösung, es entweder synchron auszuführen oder 'Task.Run' zu verwenden, wenn Sie es in einem Thread-Pool-Thread ausführen wollen. Wenn es E/A-basiert oder als Ergebnis eines externen Ereignisses abgeschlossen ist, verwenden Sie 'TaskFactory.FromAsync' oder 'TaskCompletionSource '. Es gibt keine anderen Möglichkeiten. –

+0

Können Sie bitte die TaskCompletionSource-Option genauer ausführen, da die Beispiele, die ich dafür finden kann, nicht in mein Szenario passen. –

5

Also, angesichts der Einschränkung, dass Sie nicht alle vorhandenen asynchronen Methoden aufrufen können und sowohl in einer asynchronen Aufgabe, die Thread.Sleep und die Console.WriteLine ausfüllen müssen, wie tun Sie es in einer Weise, die ist so effizient wie der ursprüngliche Code?

IMO, ist dies eine sehr synthetische Einschränkung, dass Sie wirklich mit Thread.Sleep haften müssen. Unter dieser Einschränkung können Sie Ihren Thread.Sleep-basierten Code immer noch geringfügig verbessern. Statt dessen:

static async Task DoSomethingAsync2(int id) { 
    await Task.Run(() => { 
     Thread.Sleep(50); 
     Console.WriteLine(@"DidSomethingAsync({0})", id); 
    }); 
} 

Sie können dies tun:

static Task DoSomethingAsync2(int id) { 
    return Task.Run(() => { 
     Thread.Sleep(50); 
     Console.WriteLine(@"DidSomethingAsync({0})", id); 
    }); 
} 

Auf diese Weise würde vermeiden Sie einen Overhead von der Compiler generierten Zustand Maschinenklasse. Es gibt einen feinen Unterschied zwischen diesen beiden Codefragmenten in how exceptions are propagated.

Wie auch immer, das ist nicht wo der Flaschenhals der Verlangsamung ist.

(es ist auch langsamer die folgende Konsole-Anwendung - wenn Sie Swap zwischen DoSomethingAsync und DoSomethingAsync2 nennen Sie einen signifikanten Unterschied in der Zeit sehen kann, dass es in Anspruch nimmt) die

Schauen wir uns ein weiteres Mal an Ihrem Hauptschleife Code:

static async Task MainAsync(String[] args) { 

    List<Task> tasks = new List<Task>(); 
    for (int i = 1; i <= 1000; i++) 
     tasks.Add(DoSomethingAsync2(i)); // Can replace with any version 
    await Task.WhenAll(tasks); 

} 

Technisch, fordert er 1000 Tasks parallel ausgeführt werden, von denen jede vermeintlich auf einem eigenen Thread auszuführen. In einem idealen Universum würden Sie erwarten, Thread.Sleep(50) 1000 Mal parallel auszuführen und das Ganze in etwa 50 ms abzuschließen.

Diese Anforderung wird nie erfüllt von der TPL-Standard Taskplaner, aus einem guten Grund: Thread ist eine wertvolle und teure Ressource. Darüber hinaus ist die tatsächliche Anzahl gleichzeitiger Operationen auf die Anzahl der CPUs/Kerne begrenzt. Also, in der Realität, mit der Standardgröße von ThreadPool, bekomme ich 21 Pool-Threads (in der Spitze) diese Operation parallel. Deshalb dauert DoSomethingAsync2/Thread.Sleep so viel länger als DoSomethingAsync/Task.Delay.DoSomethingAsync blockiert einen Pool-Thread nicht, er fordert nur einen nach Ablauf des Timeouts an. Somit können mehr DoSomethingAsync Tasks parallel laufen, als DoSomethingAsync2.

Der Test (eine Konsole app):

// https://stackoverflow.com/q/21800450/1768303 

using System; 
using System.Collections.Generic; 
using System.Diagnostics; 
using System.Threading; 
using System.Threading.Tasks; 

namespace Console_21800450 
{ 
    public class Program 
    { 
     static async Task DoSomethingAsync(int id) 
     { 
      await Task.Delay(50); 
      UpdateMaxThreads(); 
      Console.WriteLine(@"DidSomethingAsync({0})", id); 
     } 

     static async Task DoSomethingAsync2(int id) 
     { 
      await Task.Run(() => 
      { 
       Thread.Sleep(50); 
       UpdateMaxThreads(); 
       Console.WriteLine(@"DidSomethingAsync2({0})", id); 
      }); 
     } 

     static async Task MainAsync(Func<int, Task> tester) 
     { 
      List<Task> tasks = new List<Task>(); 
      for (int i = 1; i <= 1000; i++) 
       tasks.Add(tester(i)); // Can replace with any version 
      await Task.WhenAll(tasks); 
     } 

     volatile static int s_maxThreads = 0; 

     static void UpdateMaxThreads() 
     { 
      var threads = Process.GetCurrentProcess().Threads.Count; 
      // not using locks for simplicity 
      if (s_maxThreads < threads) 
       s_maxThreads = threads; 
     } 

     static void TestAsync(Func<int, Task> tester) 
     { 
      s_maxThreads = 0; 
      var stopwatch = new Stopwatch(); 
      stopwatch.Start(); 

      MainAsync(tester).Wait(); 

      Console.WriteLine(
       "time, ms: " + stopwatch.ElapsedMilliseconds + 
       ", threads at peak: " + s_maxThreads); 
     } 

     static void Main() 
     { 
      Console.WriteLine("Press enter to test with Task.Delay ..."); 
      Console.ReadLine(); 
      TestAsync(DoSomethingAsync); 
      Console.ReadLine(); 

      Console.WriteLine("Press enter to test with Thread.Sleep ..."); 
      Console.ReadLine(); 
      TestAsync(DoSomethingAsync2); 
      Console.ReadLine(); 
     } 

    } 
} 

Ausgang:

 
Press enter to test with Task.Delay ... 
... 
time, ms: 1077, threads at peak: 13 

Press enter to test with Thread.Sleep ... 
... 
time, ms: 8684, threads at peak: 21 

Ist es möglich, den Zeitpunkt Wert des Thread.Sleep -basierte DoSomethingAsync2 zu verbessern? Die einzige Art, wie ich denken kann, ist TaskCreationOptions.LongRunning zu verwenden, um mit Task.Factory.StartNew:

Sie sollten zweimal überlegen, bevor diese in irgendeiner realen Anwendung tun:

static async Task DoSomethingAsync2(int id) 
{ 
    await Task.Factory.StartNew(() => 
    { 
     Thread.Sleep(50); 
     UpdateMaxThreads(); 
     Console.WriteLine(@"DidSomethingAsync2({0})", id); 
    }, TaskCreationOptions.LongRunning | TaskCreationOptions.PreferFairness); 
} 

// ... 

static void Main() 
{ 
    Console.WriteLine("Press enter to test with Task.Delay ..."); 
    Console.ReadLine(); 
    TestAsync(DoSomethingAsync); 
    Console.ReadLine(); 

    Console.WriteLine("Press enter to test with Thread.Sleep ..."); 
    Console.ReadLine(); 
    TestAsync(DoSomethingAsync2); 
    Console.ReadLine(); 
} 

Ausgang:

 
Press enter to test with Thread.Sleep ... 
... 
time, ms: 3600, threads at peak: 163 

Das Timing wird besser, aber der Preis dafür ist hoch. Dieser Code fordert den Taskplaner auf, einen neuen Thread für jede neue Aufgabe zu erstellen. Sie nicht erwarten, dass dieser Thread aus dem Pool kommen:

Task.Factory.StartNew(() => 
{ 
    Thread.Sleep(1000); 
    Console.WriteLine("Thread pool: " + 
     Thread.CurrentThread.IsThreadPoolThread); // false! 
}, TaskCreationOptions.LongRunning).Wait(); 
Verwandte Themen