¿Por qué TaskScheduler.Current es TaskScheduler por defecto?

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 que TaskScheduler.Current es el planificador de tareas predeterminado, no TaskScheduler.Default . Esto no es del todo obvio a primera vista en la documentación ni en las muestras.

Current puede conducir a errores sutiles, ya que su comportamiento cambia dependiendo de si está dentro de otra tarea. Que no se puede determinar fácilmente.

Supongamos 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 paralela de tareas para su implementación porque es realmente 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*/); ... } 

Aquí, la persona que escribe la llamada a la biblioteca elige comenzar una nueva Task cuando la operación finaliza. Nada inusual. Sigue los ejemplos que se encuentran en todas partes en la web y simplemente usa Task.Factory.StartNew sin especificar TaskScheduler (y no hay una sobrecarga fácil para especificarlo en el segundo parámetro). El método DoSomethingElse funciona bien cuando se llama solo, pero tan pronto como el evento lo invoca, la interfaz de usuario se congela ya que TaskFactory.Current reutilizará el progtwigdor de tareas de contexto de sincronización de la continuación de mi biblioteca.

Descubrir esto podría tomar algo de tiempo, especialmente si la segunda llamada de tarea está enterrada en alguna stack de llamadas complejas. Por supuesto, la solución aquí es simple una vez que sepa cómo funciona todo: siempre especifique TaskScheduler.Default para cualquier operación que esté esperando ejecutar en el grupo de subprocesos. Sin embargo, tal vez la segunda tarea sea iniciada por otra biblioteca externa, sin saber sobre este comportamiento e ingenuamente usando StartNew sin un planificador específico. Espero que este caso sea bastante común.

Después de entenderlo, no puedo entender la elección del equipo que escribe el TPL para usar TaskScheduler.Current lugar de TaskScheduler.Default como predeterminado:

  • ¡No es nada obvio, el valor Default no es el predeterminado! Y la documentación falta seriamente.
  • ¡El progtwigdor de tareas reales utilizado por Current depende de la stack de llamadas! Es difícil mantener invariantes con este comportamiento.
  • Es engorroso especificar el planificador de tareas con StartNew ya que StartNew debe especificar las opciones de creación de tareas y el token de cancelación, lo que lleva a líneas largas y menos legibles. Esto se puede aliviar escribiendo un método de extensión o creando una TaskFactory que use Default .
  • La captura de la stack de llamadas tiene costos de rendimiento adicionales.
  • 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 stack de llamadas.

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

Creo que el comportamiento actual tiene sentido. Si creo mi propio planificador de tareas y comienzo alguna tarea que inicie otras tareas, probablemente quiera que todas las tareas usen el progtwigdor que creé.

Estoy de acuerdo en que es extraño que, a veces, comenzar una tarea desde el subproceso UI use el planificador predeterminado y, a veces, no. Pero no sé cómo lo mejoraría si estuviera diseñando.

En cuanto a tus problemas específicos:

  • Creo que la forma más fácil de comenzar una nueva tarea en un planificador específico es la new Task(lambda).Start(scheduler) . Esto tiene la desventaja de que debe especificar el argumento de tipo si la tarea devuelve algo. TaskFactory.Create puede inferir el tipo para ti.
  • Puede usar Dispatcher.Invoke() lugar de usar TaskScheduler.FromCurrentSynchronizationContext() .

[EDIT] Lo siguiente solo resuelve el problema con el progtwigdor utilizado por Task.Factory.StartNew .
Sin embargo, Task.ContinueWith tiene un TaskScheduler.Current codificado.Current. [/EDITAR]

Primero, hay una solución fácil disponible, vea la parte inferior de esta publicación.

La razón detrás de este problema es simple: no solo hay un progtwigdor de tareas predeterminado ( TaskScheduler.Default ) sino también un planificador de tareas predeterminado para TaskFactory ( TaskFactory.Scheduler ). Este planificador predeterminado se puede especificar en el constructor de TaskFactory cuando se crea.

Sin embargo, TaskFactory detrás de Task.Factory se crea de la siguiente manera:

 s_factory = new TaskFactory(); 

Como puede ver, no se especifica TaskFactory ; null se usa para el constructor predeterminado; mejor sería TaskScheduler.Default (la documentación indica que se usa “Actual” que tiene las mismas consecuencias).
Esto nuevamente conduce a la implementación de TaskFactory.DefaultScheduler (un miembro privado):

 private TaskScheduler DefaultScheduler { get { if (m_defaultScheduler == null) return TaskScheduler.Current; else return m_defaultScheduler; } } 

Aquí debería ver ser capaz de reconocer el motivo de este comportamiento: Como Task.Factory no tiene un planificador de tareas predeterminado, se usará el actual.

Entonces, ¿por qué no nos NullReferenceExceptions con NullReferenceExceptions , cuando ninguna tarea se está ejecutando actualmente (es decir, no tenemos TaskScheduler actual)?
La razón es simple:

 public static TaskScheduler Current { get { Task internalCurrent = Task.InternalCurrent; if (internalCurrent != null) { return internalCurrent.ExecutingTaskScheduler; } return Default; } } 

TaskScheduler.Current predeterminado en TaskScheduler.Default .

Yo llamaría a esto una implementación muy desafortunada.

Sin embargo, hay una solución fácil disponible: simplemente podemos establecer el TaskScheduler predeterminado de Task.Factory a TaskScheduler.Default

 TaskFactory factory = Task.Factory; factory.GetType().InvokeMember("m_defaultScheduler", BindingFlags.SetField | BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.DeclaredOnly, null, factory, new object[] { TaskScheduler.Default }); 

Espero poder ayudar con mi respuesta aunque es bastante tarde 🙂

En lugar de Task.Factory.StartNew()

Considere usar: Task.Run()

Esto siempre se ejecutará en un subproceso de grupo de subprocesos. Acabo de describir el mismo problema en la pregunta y creo que es una buena forma de manejar esto.

Vea esta entrada del blog: http://blogs.msdn.com/b/pfxteam/archive/2011/10/24/10229468.aspx

¡No es nada obvio, el valor predeterminado no es el predeterminado! Y la documentación falta seriamente.

Default valor Default es el predeterminado, pero no siempre es el Current .

Como ya han respondido otros, si desea que una tarea se ejecute en el grupo de subprocesos, debe establecer explícitamente el planificador Current pasando el planificador Default al método TaskFactory o StartNew .

Sin embargo, dado que su pregunta incluía una biblioteca, creo que la respuesta es que no debe hacer nada que cambie el progtwigdor Current que se ve por el código fuera de su biblioteca. Eso significa que no debe usar TaskScheduler.FromCurrentSynchronizationContext() cuando SomeOperationCompleted evento SomeOperationCompleted . En cambio, haz algo como esto:

 public void DoSomeOperationAsync() { var context = SynchronizationContext.Current; Task.Factory .StartNew(() => Thread.Sleep(1000) /* simulate a long operation */) .ContinueWith(t => { context.Post(_ => OnSomeOperationCompleted(), null); }); } 

Ni siquiera creo que deba iniciar su tarea explícitamente en el planificador Default : deje que el que llama determine el planificador Current si lo desea.

Acabo de pasar horas intentando depurar un problema extraño en el que mi tarea estaba progtwigda en el hilo de la interfaz de usuario, aunque no lo especifiqué. Resultó que el problema era exactamente lo que demostró el código de muestra: se programó una continuación de tarea en el subproceso de UI, y en algún lugar de esa continuación, se inició una nueva tarea que luego se programó en el subproceso de UI, porque la tarea que se estaba ejecutando tenía un conjunto específico TaskScheduler .

Afortunadamente, es todo código que poseo, así que puedo solucionarlo asegurándome de que mi código especifique TaskScheduler.Default al comenzar nuevas tareas, pero si no tiene tanta suerte, mi sugerencia sería usar Dispatcher.BeginInvoke lugar de usar la interfaz de usuario. progtwigdor.

Entonces, en lugar de:

 var uiScheduler = TaskScheduler.FromCurrentSynchronizationContext(); var task = Task.Factory.StartNew(() => Thread.Sleep(5000)); task.ContinueWith((t) => UpdateUI(), uiScheduler); 

Tratar:

 var uiDispatcher = Dispatcher.CurrentDispatcher; var task = Task.Factory.StartNew(() => Thread.Sleep(5000)); task.ContinueWith((t) => uiDispatcher.BeginInvoke(new Action(() => UpdateUI()))); 

Aunque es un poco menos legible.