¿Por qué TaskScheduler? ¿El actual TaskScheduler actual?

La Biblioteca paralela de tareas es excelente y la he usado mucho en los últimos meses. Sin embargo, hay algo que realmente me molesta: el hecho de queTaskScheduler.Current es el programador de tareas predeterminado, noTaskScheduler.Default. Esto no es absolutamente obvio a primera vista en la documentación ni en las muestras.

Current puede provocar errores sutiles, ya que su comportamiento cambia dependiendo de si estás dentro de otra tarea. Lo cual no se puede determinar fácilmente.

Suponga que estoy escribiendo una biblioteca de métodos asincrónicos, utilizando el patrón asíncrono estándar basado en eventos para indicar la finalización en el contexto de sincronización original, exactamente de la misma manera que lo hacen los métodos XxxAsync en .NET Framework (por ejemplo,DownloadFileAsync). Decido usar la Biblioteca de tareas paralelas para la implementación porque es muy fácil implementar este comportamiento con el siguiente código:

public class MyLibrary {
    public event EventHandler SomeOperationCompleted;

    private void OnSomeOperationCompleted() {
        var handler = SomeOperationCompleted;
        if (handler != null)
            handler(this, EventArgs.Empty);
    }

    public void DoSomeOperationAsync() {
                    Task.Factory
                        .StartNew
                         (
                            () => Thread.Sleep(1000) // simulate a long operation
                            , CancellationToken.None
                            , TaskCreationOptions.None
                            , TaskScheduler.Default
                          )
                        .ContinueWith
                           (t => OnSomeOperationCompleted()
                            , TaskScheduler.FromCurrentSynchronizationContext()
                            );
    }
}

Hasta ahora, todo funciona bien. Ahora, hagamos una llamada a esta biblioteca con un clic de botón en una aplicación WPF o WinForms:

private void Button_OnClick(object sender, EventArgs args) {
    var myLibrary = new MyLibrary();
    myLibrary.SomeOperationCompleted += (s, e) => DoSomethingElse();
    myLibrary.DoSomeOperationAsync();
}

private void DoSomethingElse() {
    ...
    Task.Factory.StartNew(() => Thread.Sleep(5000)/*simulate a long operation*/);
    ...
}

quí, la persona que escribió la llamada a la biblioteca decidió comenzar una nuevaTask cuando se completa la operación. Nada inusual. Él o ella siguen ejemplos que se encuentran en todas partes en la web y simplemente usanTask.Factory.StartNew sin especificar elTaskScheduler (y no hay sobrecarga fácil para especificarlo en el segundo parámetro). LosDoSomethingElsel método @ funciona bien cuando se llama solo, pero tan pronto como es invocado por el evento, la IU se congela desdeTaskFactory.Current reutilizará el programador de tareas de contexto de sincronización desde la continuación de mi biblioteca.

Descubrir esto podría llevar algún tiempo, especialmente si la segunda llamada de tarea está enterrada en una pila de llamadas compleja. Por supuesto, la solución aquí es simple una vez que sepa cómo funciona todo: siempre especifiqueTaskScheduler.Default para cualquier operación que espere ejecutar en el grupo de subprocesos. Sin embargo, tal vez la segunda tarea sea iniciada por otra biblioteca externa, sin conocer este comportamiento y usando ingenuamenteStartNew sin un planificador específico. Espero que este caso sea bastante común.

Después de comprenderlo, no puedo entender la elección del equipo que escribe el TPL para usarTaskScheduler.Current en lugar deTaskScheduler.Default por defecto:

No es obvio en absoluto,Default@ no es el predeterminado! Y la documentación es muy deficiente. El programador de tareas real utilizado porCurrent depende de la pila de llamadas! Es difícil mantener invariantes con este comportamiento. Es engorroso especificar el programador de tareas conStartNew ya que primero debe especificar las opciones de creación de tareas y el token de cancelación, lo que genera líneas largas y menos legibles. Esto se puede aliviar escribiendo un método de extensión o creando unTaskFactory que usaDefault.a captura de la pila de llamadas tiene costos de rendimiento adicionale Cuando realmente quiero que una tarea dependa de otra tarea principal en ejecución, prefiero especificarla explícitamente para facilitar la lectura del código en lugar de confiar en la magia de la pila de llamadas.

Sé que esta pregunta puede sonar bastante subjetiva, pero no puedo encontrar un buen argumento objetivo de por qué este comportamiento es así. Estoy seguro de que me falta algo aquí: por eso me estoy volviendo hacia ti.

Respuestas a la pregunta(5)

Su respuesta a la pregunta