2013-03-15 13 views
32

Ich habe einige Methoden, die Task<T> zurückgeben, auf die ich await nach Belieben kann. Ich möchte, dass diese Aufgaben auf einer benutzerdefinierten TaskScheduler statt der Standardausführung ausgeführt werden.Wie führe ich einen Task auf einem benutzerdefinierten TaskScheduler aus?

var task = GetTaskAsync(); 
await task; 

Ich weiß, dass ich eine neue TaskFactory (new CustomScheduler()) erstellen und eine StartNew() daraus machen, aber StartNew() nimmt eine Aktion und erstellen Sie die Task, und ich habe bereits die Task (zurück hinter den Kulissen von einem TaskCompletionSource)

Wie kann ich meinen eigenen TaskScheduler für await angeben?

+1

Warten Sie nicht auf eine heiße (aka bereits laufende) Aufgabe? Außerdem können Tasks von TaskCompletionSource nicht "ausgeführt" werden, da Sie für das Abschließen der Task verantwortlich sind, indem Sie SetResult aufrufen. Meinst du, wie man den Scheduler spezifiziert, auf dem die Fortsetzung von Warten ausgeführt wird? – sanosdole

+0

die Aufgabe ist heiß und läuft, während von der CTS zurückgegeben. Es gibt jedoch eine zeitliche Planung zwischen dem SetResult und der Fortsetzung nach dem Warten. Ich muss diese Planung mit meinem eigenen Scheduler steuern, wenn möglich –

+0

@StephaneDelcroix Können Sie nicht die gesamte Methode auf Ihrem Scheduler ausführen? Ich denke, das wäre die beste Lösung. – svick

Antwort

1

Nach den Kommentaren sieht es so aus, als ob Sie den Scheduler steuern möchten, auf dem der Code nach dem Warten ausgeführt wird.

Die Kompilierung erstellt eine Fortsetzung von der Wartezeit, die standardmäßig auf dem aktuellen SynchronizationContext ausgeführt wird. So ist dein bestes Schießen, das SynchronizationContext vor dem Anrufen vorzubereiten, zu warten.

Es gibt einige Möglichkeiten, auf einen bestimmten Kontext zu warten. Vgl. Configure Await von Jon Skeet, speziell den Teil über SwitchTo, um mehr darüber zu erfahren, wie man so etwas implementiert.

EDIT: Die SwitchTo-Methode aus TaskEx wurde entfernt, da sie zu leicht zu missbrauchen war. Siehe die MSDN Forum aus Gründen.

33

Ich denke, was Sie wirklich wollen, ist eine Task.Run zu tun, aber mit einem benutzerdefinierten Scheduler. StartNew funktioniert nicht intuitiv mit asynchronen Methoden; Stephen Toub hat einen tollen Blog-Eintrag über the differences between Task.Run and TaskFactory.StartNew.

Also, Ihre eigenen erstellen Run, können Sie etwas tun:

private static readonly TaskFactory myTaskFactory = new TaskFactory(
    CancellationToken.None, TaskCreationOptions.DenyChildAttach, 
    TaskContinuationOptions.None, new MyTaskScheduler()); 
private static Task RunOnMyScheduler(Func<Task> func) 
{ 
    return myTaskFactory.StartNew(func).Unwrap(); 
} 
private static Task<T> RunOnMyScheduler<T>(Func<Task<T>> func) 
{ 
    return myTaskFactory.StartNew(func).Unwrap(); 
} 
private static Task RunOnMyScheduler(Action func) 
{ 
    return myTaskFactory.StartNew(func); 
} 
private static Task<T> RunOnMyScheduler<T>(Func<T> func) 
{ 
    return myTaskFactory.StartNew(func); 
} 

Dann können Sie synchrone oder asynchrone Methoden auf Ihrem benutzerdefinierten Scheduler ausführen.

+0

Es tut mir leid zu sagen, aber die eigentliche Aufgabe zu tun ist auf dem Standard 'ThreadPoolTaskScheduler 'geplant - Ihr' MyTaskScheduler' ist nur verwendet, um die innere Aufgabe zu planen. – springy76

+1

@ springy76: Nein, das ist falsch. 'MyTaskScheduler' führt' func' aus, und im Falle eines asynchronen 'func' werden alle' '' '' '' '' '' '' '' '' 'MyTaskScheduler'' standardmäßig auf 'MyTaskScheduler' gesetzt. –

+0

Ich nahm das Beispiel 'LimitedConcurrencyLevelTaskScheduler' und änderte es zu LIFO. Aber alle Aufgaben werden in FIFO-Reihenfolge ausgeführt und sind nicht begrenzt, aber "TaskScheduler.Current" gibt meinen benutzerdefinierten Scheduler an jedem möglichen Debugging-Punkt zurück. Der Scheduler funktioniert wie erwartet, wenn er mit 'Parallel.ForEach' verwendet wird oder wenn eine echte' Func 'mit' myTaskFactory.StartNew() 'ausgeführt wird - aber die Kombination von' Func > 'und' Unwrap() ' führt den Code nicht einmal vor dem ersten "erwarten" weder in der richtigen Reihenfolge noch mit beschränkter Nebenläufigkeit aus und sagt, dass der Scheduler vollständig ignoriert wird. – springy76

7

The TaskCompletionSource<T>.Task wird ohne eine Aktion ausgeführt und die Ablaufsteuerung ist auf dem ersten Aufruf von ContinueWith(...) (von Asynchronous Programming with the Reactive Framework and the Task Parallel Library — Part 3) zugeordnet.

Zum Glück können Sie das await Verhalten leicht anpassen, indem Sie Ihre eigene Klasse ZUSAMMENHANG INotifyCompletion Implementierung und Verwendung sie dann in einem Muster ähnlich await SomeTask.ConfigureAwait(false) den Scheduler zu konfigurieren, dass die Aufgabe sollte mit der OnCompleted(Action continuation) Methode (von await anything;) starten.Hier

ist die Nutzung:

TaskCompletionSource<object> source = new TaskCompletionSource<object>(); 

    public async Task Foo() { 
     // Force await to schedule the task on the supplied scheduler 
     await SomeAsyncTask().ConfigureScheduler(scheduler); 
    } 

    public Task SomeAsyncTask() { return source.Task; } 

Hier ist eine einfache Implementierung von ConfigureScheduler eine Task-Extension-Methode mit dem wichtigen Teil unter Verwendung von in OnCompleted:

public static class TaskExtension { 
    public static CustomTaskAwaitable ConfigureScheduler(this Task task, TaskScheduler scheduler) { 
     return new CustomTaskAwaitable(task, scheduler); 
    } 
} 

public struct CustomTaskAwaitable { 
    CustomTaskAwaiter awaitable; 

    public CustomTaskAwaitable(Task task, TaskScheduler scheduler) { 
     awaitable = new CustomTaskAwaiter(task, scheduler); 
    } 

    public CustomTaskAwaiter GetAwaiter() { return awaitable; } 

    public struct CustomTaskAwaiter : INotifyCompletion { 
     Task task; 
     TaskScheduler scheduler; 

     public CustomTaskAwaiter(Task task, TaskScheduler scheduler) { 
      this.task = task; 
      this.scheduler = scheduler; 
     } 

     public void OnCompleted(Action continuation) { 
      // ContinueWith sets the scheduler to use for the continuation action 
      task.ContinueWith(x => continuation(), scheduler); 
     } 

     public bool IsCompleted { get { return task.IsCompleted; } } 
     public void GetResult() { } 
    } 
} 

Hier ist eine Arbeitsprobe, die wie kompilieren eine Konsolenanwendung:

using System; 
using System.Collections.Generic; 
using System.Runtime.CompilerServices; 
using System.Threading.Tasks; 

namespace Example { 
    class Program { 
     static TaskCompletionSource<object> source = new TaskCompletionSource<object>(); 
     static TaskScheduler scheduler = new CustomTaskScheduler(); 

     static void Main(string[] args) { 
      Console.WriteLine("Main Started"); 
      var task = Foo(); 
      Console.WriteLine("Main Continue "); 
      // Continue Foo() using CustomTaskScheduler 
      source.SetResult(null); 
      Console.WriteLine("Main Finished"); 
     } 

     public static async Task Foo() { 
      Console.WriteLine("Foo Started"); 
      // Force await to schedule the task on the supplied scheduler 
      await SomeAsyncTask().ConfigureScheduler(scheduler); 
      Console.WriteLine("Foo Finished"); 
     } 

     public static Task SomeAsyncTask() { return source.Task; } 
    } 

    public struct CustomTaskAwaitable { 
     CustomTaskAwaiter awaitable; 

     public CustomTaskAwaitable(Task task, TaskScheduler scheduler) { 
      awaitable = new CustomTaskAwaiter(task, scheduler); 
     } 

     public CustomTaskAwaiter GetAwaiter() { return awaitable; } 

     public struct CustomTaskAwaiter : INotifyCompletion { 
      Task task; 
      TaskScheduler scheduler; 

      public CustomTaskAwaiter(Task task, TaskScheduler scheduler) { 
       this.task = task; 
       this.scheduler = scheduler; 
      } 

      public void OnCompleted(Action continuation) { 
       // ContinueWith sets the scheduler to use for the continuation action 
       task.ContinueWith(x => continuation(), scheduler); 
      } 

      public bool IsCompleted { get { return task.IsCompleted; } } 
      public void GetResult() { } 
     } 
    } 

    public static class TaskExtension { 
     public static CustomTaskAwaitable ConfigureScheduler(this Task task, TaskScheduler scheduler) { 
      return new CustomTaskAwaitable(task, scheduler); 
     } 
    } 

    public class CustomTaskScheduler : TaskScheduler { 
     protected override IEnumerable<Task> GetScheduledTasks() { yield break; } 
     protected override bool TryExecuteTaskInline(Task task, bool taskWasPreviouslyQueued) { return false; } 
     protected override void QueueTask(Task task) { 
      TryExecuteTask(task); 
     } 
    } 
} 
+3

Während es möglich ist, dies zu tun, ist es im Produktionscode normalerweise keine gute Idee. Das ursprüngliche CTP hatte einen "SwitchTo" -Operator, der sich auf die gleiche Weise verhielt, aber entfernt wurde, weil er einen unstrukturierten Programmfluss förderte, was wiederum Komplikationen und Fallstricke verursachte. Wenn Sie zum Beispiel ein modifiziertes 'wait' in Ihrem' try' verwenden, müsste Ihr 'finally' Block entweder auf dem ursprünglichen Scheduler oder dem modifizierten Scheduler ausgeführt werden können (und der' finally' Code kann die Scheduler seitdem nicht wechseln) 'erwarten' ist nicht erlaubt). –

+0

@StephenCleary jetzt, dass catch/finally blockieren kann (C# 6), ist es immer noch eine schlechte Idee? – Kryptos

+0

@Kryptos: Ja, es ist immer noch eine schlechte Idee. Das Aktivieren von 'await' in' catch'/'finally' hat keinen Einfluss auf das' SwitchTo'-Verhalten. –

2

Können Sie für diesen Methodenaufruf passen:

await Task.Factory.StartNew(
     () => { /* to do what you need */ }, 
     CancellationToken.None, /* you can change as you need */ 
     TaskCreationOptions.None, /* you can change as you need */ 
     customScheduler); 
Verwandte Themen