Tarea. Rendimiento – usos reales?

He estado leyendo sobre Task.Yield , y como desarrollador de Javascript puedo decir que su trabajo es exactamente igual que setTimeout(function (){...},0); en términos de dejar que el hilo principal principal trate con otras cosas, también conocido como:

“No tomes todo el poder, libéralo de vez en cuando, para que otros también lo tengan …”

En js está funcionando particularmente en bucles largos. ( no congelar el navegador … )

Pero vi este ejemplo aquí :

 public static async Task  FindSeriesSum(int i1) { int sum = 0; for (int i = 0; i < i1; i++) { sum += i; if (i % 1000 == 0) ( after a bulk , release power to main thread) await Task.Yield(); } return sum; } 

Como progtwigdor de JS puedo entender lo que hicieron aquí.

PERO como progtwigdor de C # me pregunto: ¿por qué no abrir una tarea para ello?

  public static async Task  FindSeriesSum(int i1) { //do something.... return await MyLongCalculationTask(); //do something } 

Pregunta

Con Js no puedo abrir una tarea ( sí, sé que puedo con trabajadores web ). Pero con c # puedo .

Si es así , ¿por qué molestarse en liberar de vez en cuando mientras puedo liberarlo?

Editar

Añadiendo referencias:

Desde aqui introduzca la descripción de la imagen aquí

Desde aquí (otro ebook):

introduzca la descripción de la imagen aquí

Cuando veas:

 await Task.Yield(); 

Puedes pensarlo de esta manera:

 await Task.Factory.StartNew( () => {}, CancellationToken.None, TaskCreationOptions.None, SynchronizationContext.Current != null? TaskScheduler.FromCurrentSynchronizationContext(): TaskScheduler.Current); 

Todo lo que hace es asegurarse de que la continuación suceda de forma asíncrona en el futuro. Por asíncrono me refiero a que el control de ejecución volverá al llamador del método async , y la callback de continuación no ocurrirá en el mismo marco de stack.

Cuando exactamente y en qué subproceso sucederá completamente depende del contexto de sincronización del subproceso de la persona que llama.

Para un subproceso de la interfaz de usuario , la continuación ocurrirá en alguna iteración futura del bucle de mensajes, ejecutada por Application.Run ( WinForms ) o Dispatcher.Run ( WPF ). Internamente, se reduce a la API Win32 PostMessage , que publica un mensaje personalizado en la cola de mensajes del subproceso de la interfaz de usuario. La llamada de continuación de continuación de await se llamará cuando este mensaje se bombee y se procese. Estás completamente fuera de control sobre cuándo va a suceder exactamente esto.

Además, Windows tiene sus propias prioridades para los mensajes de bombeo: INFORMACIÓN: Prioridades de mensajes de la ventana . La parte más relevante:

Bajo este esquema, la priorización puede considerarse de tres niveles. Todos los mensajes publicados tienen mayor prioridad que los mensajes de entrada del usuario porque residen en colas diferentes. Y todos los mensajes de entrada del usuario tienen mayor prioridad que los mensajes WM_PAINT y WM_TIMER.

Por lo tanto, si usa await Task.Yield() para ceder al bucle de mensajes en un bash de mantener la interfaz de usuario receptiva, en realidad corre el riesgo de obstruir el bucle de mensajes del subproceso de la interfaz de usuario. Algunos mensajes de entrada de usuario pendientes, así como WM_PAINT y WM_TIMER , tienen una prioridad más baja que el mensaje de continuación publicado. Por lo tanto, si await Task.Yield() en un bucle cerrado, todavía puede bloquear la interfaz de usuario.

Así es como es diferente de la analogía de setTimer de JavaScript que mencionó en la pregunta. Se setTimer callback de setTimer después de que todos los mensajes de entrada del usuario hayan sido procesados ​​por la bomba de mensajes del navegador.

Por lo tanto, await Task.Yield() no es bueno para hacer un trabajo de fondo en el hilo de la interfaz de usuario. De hecho, rara vez necesita ejecutar un proceso en segundo plano en el subproceso de la IU, pero a veces lo hace, por ejemplo, el resaltado de syntax del editor, la corrección ortográfica, etc. En este caso, use la infraestructura inactiva del marco.

Por ejemplo, con WPF podrías await Dispatcher.Yield(DispatcherPriority.ApplicationIdle) :

 async Task DoUIThreadWorkAsync(CancellationToken token) { var i = 0; while (true) { token.ThrowIfCancellationRequested(); await Dispatcher.Yield(DispatcherPriority.ApplicationIdle); // do the UI-related work item this.TextBlock.Text = "iteration " + i++; } } 

Para WinForms, podrías usar el evento Application.Idle :

 // await IdleYield(); public static Task IdleYield() { var idleTcs = new TaskCompletionSource(); // subscribe to Application.Idle EventHandler handler = null; handler = (s, e) => { Application.Idle -= handler; idleTcs.SetResult(true); }; Application.Idle += handler; return idleTcs.Task; } 

Se recomienda que no exceda los 50 ms en cada iteración de dicha operación en segundo plano que se ejecuta en el subproceso de la interfaz de usuario.

Para un subproceso sin interfaz de usuario sin contexto de sincronización, await Task.Yield() simplemente cambia la continuación a un subproceso de grupo aleatorio. No hay garantía de que va a ser un hilo diferente del hilo actual, solo se garantiza que sea una continuación asíncrona . Si ThreadPool está muriendo de hambre, puede progtwigr la continuación en el mismo hilo.

En ASP.NET , hacer await Task.Yield() no tiene ningún sentido, excepto por la solución que se menciona en la respuesta de @ StephenCleary . De lo contrario, solo afectará el rendimiento de la aplicación web con un cambio de subproceso redundante.

Entonces, es await Task.Yield() útil? OMI, no mucho. Puede usarse como acceso directo para ejecutar la continuación a través de SynchronizationContext.Post o ThreadPool.QueueUserWorkItem , si realmente necesita imponer la asincronía en una parte de su método.

Con respecto a los libros que citó , en mi opinión, esos enfoques para usar Task.Yield son incorrectos. Le expliqué por qué están equivocados en un hilo de UI, arriba. Para un subproceso no agrupado de UI, simplemente no hay “otras tareas en el subproceso para ejecutar” , a menos que esté ejecutando una bomba de tareas personalizada como AsyncPump Stephen Toub .

Actualizado para responder al comentario:

… ¿cómo puede ser una operación asíncrona y permanecer en el mismo hilo? ..

Como un simple ejemplo: la aplicación WinForms:

 async void Form_Load(object s, object e) { await Task.Yield(); MessageBox.Show("Async message!"); } 

Form_Load regresará a la persona que llama (el código de marco de WinFroms que ha activado el evento de Load ), y luego se mostrará el cuadro de mensaje de forma asíncrona, en una futura iteración del bucle de mensajes ejecutado por Application.Run() . La callback de continuación se pone en cola con WinFormsSynchronizationContext.Post , que publica internamente un mensaje privado de Windows en el bucle de mensajes del subproceso de la interfaz de usuario. La callback se ejecutará cuando este mensaje se bombee, aún en el mismo hilo.

En una aplicación de consola, puede ejecutar un bucle de serialización similar con AsyncPump mencionado anteriormente.

Solo encontré Task.Yield útil en dos escenarios:

  1. Pruebas unitarias, para asegurar que el código bajo prueba funcione apropiadamente en presencia de asincronía.
  2. Para solucionar un problema de ASP.NET oscuro donde el código de identidad no se puede completar de forma síncrona .

No, no es exactamente como usar setTimeout para devolver el control a la interfaz de usuario. En Javascript, eso siempre permitiría que la UI se actualice, ya que setTimeout siempre tiene una pausa mínima de unos pocos milisegundos, y el trabajo pendiente de IU tiene prioridad sobre los temporizadores, pero await Task.Yield(); no hace eso

No hay garantía de que el rendimiento permita que se realice ningún trabajo en el subproceso principal, al contrario, el código que llamó el rendimiento a menudo se priorizará sobre el trabajo de UI.

“El contexto de sincronización que está presente en un subproceso de UI en la mayoría de los entornos de UI a menudo priorizará el trabajo publicado en el contexto más alto que el trabajo de entrada y representación. Por esta razón, no confíe en esperar Task.Yield (); para mantener una IU sensible . ”

Ref: MSDN: Método Task.Yield

En primer lugar, permítame aclarar: el Yield no es exactamente el mismo que setTimeout(function (){...},0); . JS se ejecuta en un entorno de un solo subproceso, por lo que es la única forma de permitir que otras actividades se realicen. Tipo de multitarea cooperativa . .net se ejecuta en un entorno multitarea preferente con multihilo explícito.

Ahora volvamos a Thread.Yield . Como le dije, .net vive en un mundo preventivo, pero es un poco más complicado que eso. C # await/async crea una mezcla interesante de los modos multitarea regidos por las máquinas de estado. Entonces, si omites el Yield de tu código, solo bloqueará el hilo y eso es todo. Si lo convierte en una tarea normal y simplemente llama a inicio (o un subproceso), entonces solo hará sus cosas en paralelo y luego bloqueará el subproceso de llamada cuando se llame a la tarea.Resultado. ¿Qué pasa cuando haces await Task.Yield(); es mas complicado Lógicamente, desbloquea el código de llamada (similar a JS) y la ejecución continúa. Lo que realmente hace: selecciona otro subproceso y continúa su ejecución en un entorno preventivo con el subproceso de llamada. Así que está en el hilo de llamada hasta la primera Task.Yield y luego es por sí solo. Llamadas posteriores a la Task.Yield aparentemente no hace nada.

Demostración simple:

 class MainClass { //Just to reduce amont of log itmes static HashSet> cache = new HashSet>(); public static void LogThread(string msg, bool clear=false) { if (clear) cache.Clear (); var val = Tuple.Create(msg, Thread.CurrentThread.ManagedThreadId); if (cache.Add (val)) Console.WriteLine ("{0}\t:{1}", val.Item1, val.Item2); } public static async Task FindSeriesSum(int i1) { LogThread ("Task enter"); int sum = 0; for (int i = 0; i < i1; i++) { sum += i; if (i % 1000 == 0) { LogThread ("Before yield"); await Task.Yield (); LogThread ("After yield"); } } LogThread ("Task done"); return sum; } public static void Main (string[] args) { LogThread ("Before task"); var task = FindSeriesSum(1000000); LogThread ("While task", true); Console.WriteLine ("Sum = {0}", task.Result); LogThread ("After task"); } } 

Aquí están los resultados:

 Before task :1 Task enter :1 Before yield :1 After yield :5 Before yield :5 While task :1 Before yield :5 After yield :5 Task done :5 Sum = 1783293664 After task :1 
  • Salida producida en mono 4.5 en Mac OS X, los resultados pueden variar en otras configuraciones

Si mueve Task.Yield sobre el método, será asíncrono desde el principio y no bloqueará el subproceso de llamada.

Conclusión: Task.Yield puede hacer posible mezclar código de sincronización y asíncrono. Un escenario más o menos realista: tiene una gran operación computacional y un caché local y CalcThing tareas. En este método, verifica si el elemento está en la memoria caché, si es así - devuelva el elemento, si no está allí. Yield y proceda a calcularlo. En realidad, la muestra de su libro no tiene sentido porque no se logra nada útil allí. Su observación con respecto a la interactividad de la GUI es mala e incorrecta (el subproceso de la IU se bloqueará hasta la primera llamada a Yield , nunca debe hacer eso, MSDN es claro (y correcto) en eso: "no confíe en esperar Task.Yield (); para mantener una interfaz de usuario sensible ".

Suponemos que la función de larga duración puede ejecutarse en un subproceso en segundo plano. Si no lo es, por ejemplo, debido a que tiene interacción con la interfaz de usuario, entonces no hay manera de evitar el locking de la interfaz de usuario mientras se ejecuta, por lo que los tiempos de ejecución deben ser lo suficientemente cortos como para no causar problemas a los usuarios.

Otra posibilidad es que tenga más funciones de ejecución larga que hilos de fondo. En ese escenario, puede ser mejor (o puede que no importe, depende) evitar que algunas de esas funciones ocupen todos sus hilos.