Considere el siguiente código:
// MyKickAssTaskScheduler is a TaskScheduler, IDisposable using (var scheduler = new MyKickAssTaskScheduler()) { Task foo = new Task(() => DoSomething()); foo.ContinueWith((_) => DoSomethingElse()); foo.Start(scheduler); foo.Wait(); } ¿Está garantizada la ejecución de la tarea ContinueWith() en mi programador? Si no, ¿qué programador usará?
StartNew, ContinueWith será predeterminado a TaskScheduler.Current, Current devolverá el programador predeterminado, cuando no se llame desde dentro de una tarea (MSDN).
Para evitar el problema del programador predeterminado, siempre debe pasar un TaskScheduler explícito a Task.ContinueWith y Task.Factory.StartNew.
¿Está garantizada la ejecución de la tarea ContinueWith() en mi programador? Si no, ¿qué programador usará?
No, utilizará el programador pasado a la Task original. ContinueWith usará de forma predeterminada TaskScheduler.Current , en cuyo caso es el programador de tareas del grupo de subprocesos predeterminado. No hay propagación entre su contexto proporcionado a task.Start y el que se usa dentro de la continuación
public Task ContinueWith(Action<Task> continuationAction) { StackCrawlMark stackMark = StackCrawlMark.LookForMyCaller; return ContinueWith(continuationAction, TaskScheduler.Current, default(CancellationToken), TaskContinuationOptions.None, ref stackMark); }@Noseratio: léalo, pero aún escéptico sobre la validez de este comportamiento: ejecuté la primera tarea en un programador no predeterminado por una razón. ¿Por qué TPL decidió que la continuación, que siempre es secuencial a mi tarea, debería ejecutarse en otra?
Estoy de acuerdo, este no es el mejor diseño, pero me imagino que el valor predeterminado de TaskScheduler.Current está ahí para que ContinueWith sea coherente con Task.Factory.StartNew , que también tiene como valor predeterminado TaskScheduler.Current , en primer lugar. Stephen Toub explica la última decisión de diseño:
En muchas situaciones, ese es el comportamiento correcto. Por ejemplo, supongamos que está implementando un problema recursivo de divide y vencerás, en el que tienes una tarea que se supone que debe procesar una parte del trabajo y, a su vez, subdivide su trabajo y programa tareas para procesar esas partes. Si esa tarea se estaba ejecutando en un programador que representaba un conjunto particular de subprocesos, o si se estaba ejecutando en un programador que tenía un límite de simultaneidad, etc., normalmente desearía que las tareas que creó también se ejecutaran en el mismo programador. .
Por lo tanto, ContinueWith usa el TaskScheduler.Current actual (ambiente) de cualquier tarea que se esté ejecutando actualmente en el momento en que llama a ContinueWith , en lugar de la de la tarea antecedente. En caso de que esto sea un problema para usted y no pueda especificar explícitamente el programador de tareas, existe una solución alternativa. Puede hacer que su programador de tareas personalizado sea el ambiental para un ámbito particular, como este:
using (var scheduler = new MyKickAssTaskScheduler()) { Task<Task> outer = new Task(() => { Task foo = new Task(() => DoSomething()); foo.ContinueWith((_) => DoSomethingElse()); foo.Start(); // don't have to specify scheduler here return foo; } outer.RunSynchronously(scheduler); outer.Unwrap().Wait(); } Tenga en cuenta que outer es Task<Task> , por lo tanto, hay outer.Unwrap() . También podrías hacer outer.Result.Wait() , pero hay alguna diferencia semántica, especialmente si outer.Start(scheduler) en lugar de outer.RunSynchronously(scheduler) .