web-dev-qa-db-fra.com

Comment exécuter une tâche sur un TaskScheduler personnalisé à l'aide de l'attente?

J'ai quelques méthodes retournant Task<T> Sur lesquelles je peux await à volonté. J'aimerais que ces tâches soient exécutées sur un TaskScheduler personnalisé au lieu de celui par défaut.

var task = GetTaskAsync ();
await task;

Je sais que je peux créer une nouvelle TaskFactory (new CustomScheduler ()) et faire une StartNew (), mais StartNew () prend une action et crée la Task, et je ont déjà le Task (retourné dans les coulisses par un TaskCompletionSource)

Comment puis-je spécifier mon propre TaskScheduler pour await?

38
Stephane Delcroix

Je pense que ce que vous voulez vraiment, c'est de faire un Task.Run, mais avec un planificateur personnalisé. StartNew ne fonctionne pas intuitivement avec les méthodes asynchrones; Stephen Toub a un excellent article de blog sur les différences entre Task.Run et TaskFactory.StartNew .

Donc, pour créer votre propre Run personnalisé, vous pouvez faire quelque chose comme ceci:

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);
}

Vous pouvez ensuite exécuter des méthodes asynchrones synchrones ou sur votre planificateur personnalisé.

41
Stephen Cleary

Le TaskCompletionSource<T>.Task Est construit sans aucune action et le planificateur est affecté lors du premier appel à ContinueWith(...) (à partir de Programmation asynchrone avec le cadre réactif et la bibliothèque parallèle de tâches - Partie ).

Heureusement, vous pouvez personnaliser légèrement le comportement d'attente en implémentant votre propre classe dérivée de INotifyCompletion, puis en l'utilisant dans un modèle similaire à await SomeTask.ConfigureAwait(false) pour configurer le planificateur que la tâche doit commencer à utiliser dans le OnCompleted(Action continuation) méthode (de attendre quoi que ce soit; ).

Voici l'utilisation:

    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; }

Voici une implémentation simple de ConfigureScheduler en utilisant une méthode d'extension de tâche avec la partie importante dans 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() { }
    }
}

Voici un exemple de travail qui se compilera en tant qu'application console:

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);
        }
    }
}
9
Adam Davidson

Après les commentaires, il semble que vous souhaitiez contrôler le planificateur sur lequel le code après l'exécution de l'attente est exécuté.

La compilation crée une continuation à partir de l'attente qui s'exécute sur le SynchronizationContext en cours par défaut. Donc, votre meilleur coup est de configurer le SynchronizationContext avant d'appeler wait.

Il y a plusieurs façons d'attendre un contexte spécifique. Voir Configure Await de Jon Skeet, en particulier la partie sur SwitchTo, pour plus d'informations sur la façon d'implémenter quelque chose comme ça.

EDIT: La méthode SwitchTo de TaskEx a été supprimée, car elle était trop facile à utiliser à mauvais escient. Voir le MSDN Forum pour des raisons.

3
sanosdole

Pouvez-vous vous adapter à cet appel de méthode:

  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);
3
Papay

Il n'y a aucun moyen d'incorporer des fonctionnalités asynchrones riches dans un --- TaskScheduler personnalisé. Cette classe n'a pas été conçue avec async/await à l'esprit. La manière standard d'utiliser un TaskScheduler personnalisé est comme argument de la méthode Task.Factory.StartNew . Cette méthode ne comprend pas les délégués asynchrones. Il est possible de fournir un délégué asynchrone, mais il est traité comme tout autre délégué qui renvoie un résultat. Pour obtenir le résultat réel attendu du délégué asynchrone, il faut appeler Unwrap() à la tâche renvoyée. Ce n'est pas le problème cependant. Le problème est que l'infrastructure TaskScheduler ne traite pas le délégué asynchrone comme une seule unité de travail. Il divise chaque tâche en plusieurs mini-tâches (en utilisant chaque await comme séparateur), et chaque mini-tâche est traitée individuellement. Cela limite considérablement la fonctionnalité asynchrone qui peut être implémentée au-dessus de cette classe. À titre d'exemple, voici un TaskScheduler personnalisé qui est destiné à mettre en file d'attente les tâches fournies une par une (pour limiter la concurrence en d'autres termes):

public class MyTaskScheduler : TaskScheduler
{
    private readonly SemaphoreSlim _semaphore = new SemaphoreSlim(1);

    protected async override void QueueTask(Task task)
    {
        await _semaphore.WaitAsync();
        try
        {
            await Task.Run(() => base.TryExecuteTask(task));
        }
        finally
        {
            _semaphore.Release();
        }
    }

    protected override bool TryExecuteTaskInline(Task task,
        bool taskWasPreviouslyQueued) => base.TryExecuteTask(task);

    protected override IEnumerable<Task> GetScheduledTasks() { yield break; }
}

Le SemaphoreSlim doit garantir qu'un seul Task s'exécute à la fois. Malheureusement ça ne marche pas. Le sémaphore est libéré prématurément, car le Task passé dans l'appel QueueTask(task) n'est pas la tâche qui représente tout le travail du délégué asynchrone, mais seulement la partie jusqu'au premier await. Les autres parties sont passées à la méthode TryExecuteTaskInline. Il n'y a aucun moyen de corréler ces tâches, car aucun identifiant ou autre mécanisme n'est fourni. Voici ce qui se passe en pratique:

var taskScheduler = new MyTaskScheduler();
var tasks = Enumerable.Range(1, 5).Select(n => Task.Factory.StartNew(async () =>
{
    Console.WriteLine($"{DateTime.Now:HH:mm:ss.fff} Item {n} Started");
    await Task.Delay(1000);
    Console.WriteLine($"{DateTime.Now:HH:mm:ss.fff} Item {n} Finished");
}, default, TaskCreationOptions.None, taskScheduler))
.Select(t => t.Unwrap())
.ToArray();
Task.WaitAll(tasks);

Production:

05: 29: 58.346 Point 1 commencé
05: 29: 58.358 Point 2 commencé
05: 29: 58.358 Point 3 commencé
05: 29: 58.358 Point 4 commencé
05: 29: 58.358 Point 5 commencé
05: 29: 59.358 Point 1 terminé
05: 29: 59.374 Point 5 terminé
05: 29: 59.374 Point 4 terminé
05: 29: 59.374 Point 2 terminé
05: 29: 59.374 Point 3 terminé

En cas de catastrophe, toutes les tâches sont mises en file d'attente à la fois.

Conclusion: Personnaliser la classe TaskScheduler n'est pas la voie à suivre lorsque des fonctionnalités asynchrones avancées sont requises.

0
Theodor Zoulias