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
?
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é.
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);
}
}
}
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.
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);
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.