c# - example - Tarea.Rendimiento: ¿usos reales?
remarks c# (5)
He estado leyendo sobre Task.Yield
, y como desarrollador de Javascript puedo decir que su trabajo es exactamente el mismo que setTimeout(function (){...},0);
en términos de dejar que el hilo principal principal se ocupe de otras cosas, también conocido como:
"No tomes todo el poder, libérate del tiempo, para que otros también lo tengan ..."
En js funciona particularmente en loops largos. ( no hagas que el navegador se congele ... )
Pero vi este ejemplo here :
public static async Task < int > 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 programador de JS puedo entender lo que hicieron aquí.
PERO como programador de C # me pregunto: ¿por qué no abrir una tarea para ello?
public static async Task < int > FindSeriesSum(int i1)
{
//do something....
return await MyLongCalculationTask();
//do something
}
Pregunta
Con Js no puedo abrir una tarea ( sí, sé que puedo hacerlo con los trabajadores de la web ). Pero con c # puedo .
Si es así , ¿por qué molestarse en soltarlo de vez en cuando mientras puedo liberarlo?
Editar
Agregar referencias:
Desde here :
Desde here (otro ebook):
Antes que nada, déjenme aclarar: El Yield
no es exactamente igual que setTimeout(function (){...},0);
. JS se ejecuta en un entorno de subproceso único, por lo que es la única forma de permitir que otras actividades sucedan. Tipo de multitarea cooperativa . .net se ejecuta en un entorno multitarea preferente con multiprocesamiento explícito.
Ahora vuelve a Thread.Yield
. Como dije, .net vive en un mundo preventivo, pero es un poco más complicado que eso. C # await/async
crea una mezcla interesante de esos modos multitarea regidos por máquinas de estado. Entonces, si omites Yield
de tu código, simplemente bloqueará el hilo y eso es todo. Si lo convierte en una tarea normal y simplemente invoca start (o un hilo), simplemente lo hará en paralelo y luego bloqueará el hilo de invocación cuando se invoque task.Result. Qué sucede cuando 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 hilo y continúa la ejecución en él en un entorno preferente con hilo de llamada. Así que está en el hilo de llamada hasta la primera Task.Yield
y luego está solo. Las llamadas posteriores a Task.Yield
aparentemente no hacen nada.
Demostración simple:
class MainClass
{
//Just to reduce amont of log itmes
static HashSet<Tuple<string, int>> cache = new HashSet<Tuple<string, int>>();
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<int> 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í hay 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, se sincronizará desde el principio y no bloqueará el hilo de llamada.
Conclusión: Task.Yield
puede hacer posible la mezcla de sincronización y código asíncrono. Algunos escenarios más o menos realistas: tiene una gran operación computacional y caché local y tarea CalcThing
. En este método, usted verifica si el artículo está en el caché, si es así, devuelva el artículo, si no está allí, Yield
y proceda a calcularlo. En realidad, la muestra de tu libro no tiene sentido porque no se logra nada útil allí. Su observación sobre la interactividad de la GUI es mala e incorrecta (el hilo de UI se bloqueará hasta la primera llamada a Yield
, nunca debes hacer eso, MSDN es claro (y correcto) sobre eso: "no confíes en esperar Task.Yield (); para mantener una UI receptiva ".
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 ocurra de manera asincrónica en el futuro. De forma asíncrona quiero decir que el control de ejecución volverá a la persona que llama del método async
, y la devolución de llamada de continuación no ocurrirá en el mismo marco de la pila.
Cuándo exactamente y en qué hilo sucederá depende completamente del contexto de sincronización del hilo de la persona que llama.
Para un subproceso de interfaz de usuario , la continuación se producirá en alguna iteración futura del ciclo de mensajes, ejecutado por Application.Run
( WinForms ) o Dispatcher.Run
( WPF ). Internamente, se reduce a la API PostMessage
Win32, que publica un mensaje personalizado en la cola de mensajes del subproceso de la interfaz de usuario. Se llamará a la devolución de llamada de continuación cuando este mensaje sea bombeado y procesado. Estás completamente fuera de control sobre cuándo exactamente va a suceder esto.
Además, Windows tiene sus propias prioridades para el bombeo de mensajes: INFO: Ventana de mensajes de prioridad . La parte más relevante:
Bajo este esquema, la priorización se puede considerar de tres niveles. Todos los mensajes publicados son de mayor prioridad que los mensajes de entrada del usuario porque residen en diferentes colas. Y todos los mensajes de entrada del usuario son de mayor prioridad que los mensajes WM_PAINT y WM_TIMER.
Por lo tanto, si utiliza await Task.Yield()
para ceder al bucle de mensajes en un intento de mantener la UI receptiva, corre el riesgo de obstruir el bucle de mensajes de la interfaz de usuario. Algunos mensajes de entrada de usuario pendientes, así como WM_PAINT
y WM_TIMER
, tienen una prioridad menor que el mensaje de continuación publicado. Por lo tanto, si await Task.Yield()
en un bucle cerrado, aún puede bloquear la interfaz de usuario.
Así es como es diferente de la analogía setTimer
de JavaScript que mencionaste en la pregunta. Se setTimer
devolución de llamada 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 en segundo plano en el subproceso de interfaz de usuario. De hecho, muy rara vez necesita ejecutar un proceso en segundo plano en el hilo de la interfaz de usuario, pero a veces lo hace, por ejemplo resaltado de sintaxis del editor, corrector ortográfico, etc. En este caso, use la infraestructura inactiva del marco.
Por ejemplo, con WPF puedes 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ía usar el evento Application.Idle
:
// await IdleYield();
public static Task IdleYield()
{
var idleTcs = new TaskCompletionSource<bool>();
// 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 para cada iteración de dicha operación de fondo que se ejecute en el hilo de la interfaz de usuario.
Para un hilo que no sea UI sin contexto de sincronización, await Task.Yield()
simplemente cambie la continuación a un hilo de grupo aleatorio. No hay garantía de que va a ser un hilo diferente del hilo actual, solo se garantiza que será una continuación asincrónica . Si ThreadPool
está muriendo de hambre, puede programar la continuación en el mismo hilo.
En ASP.NET , await Task.Yield()
no tenga ningún sentido, a excepción de la solución alternativa mencionada en la respuesta de @ StephenCleary . De lo contrario, solo dañará el rendimiento de la aplicación web con un conmutador de subproceso redundante.
Entonces, ¿está await Task.Yield()
útil? IMO, no mucho. Se puede usar como acceso directo para ejecutar la continuación a través de SynchronizationContext.Post
o ThreadPool.QueueUserWorkItem
, si realmente necesita imponer 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. Expliqué por qué están equivocados para un hilo de UI, arriba. Para un hilo de agrupación no UI, simplemente no hay "otras tareas en el hilo para ejecutar" , a menos que ejecute una bomba de tarea personalizada como AsyncPump
Stephen Toub .
Actualizado para responder el comentario:
... ¿cómo puede ser una operación asynchronouse y permanecer en el mismo hilo? ...
Como un simple ejemplo: aplicación WinForms:
async void Form_Load(object s, object e)
{
await Task.Yield();
MessageBox.Show("Async message!");
}
Form_Load
volverá a la persona que llama (el código de marco WinFroms que ha disparado el evento Load
), y luego el cuadro de mensaje se mostrará de forma asincrónica, en alguna iteración futura del ciclo de mensajes ejecutado por Application.Run()
. La devolución de llamada de continuación se pone en cola con WinFormsSynchronizationContext.Post
, que internamente publica un mensaje privado de Windows en el bucle de mensajes del subproceso de la interfaz de usuario. La devolución de llamada se ejecutará cuando se bombea este mensaje, aún en el mismo hilo.
En una aplicación de consola, puede ejecutar un bucle de serialización similar con AsyncPump
mencionado anteriormente.
Está asumiendo que la función de ejecución prolongada es aquella que puede ejecutarse en una cadena de fondo. Si no es así, por ejemplo porque tiene interacción de UI, entonces no hay forma de evitar el bloqueo de la UI mientras se ejecuta, por lo que las veces que se ejecuta deben mantenerse lo suficientemente cortas como para no causar problemas a los usuarios.
Otra posibilidad es que tenga más funciones de larga ejecución que las que tiene hilos de fondo. En ese escenario, puede ser mejor (o puede no importar, depende) evitar que algunas de esas funciones ocupen todos sus hilos.
No, no es exactamente como usar setTimeout
para devolver el control a la interfaz de usuario. En Javascript eso siempre deja que la UI se actualice ya que el setTimeout
siempre tiene una pausa mínima de unos pocos milisegundos, y el trabajo de UI pendiente tiene prioridad sobre los temporizadores, pero await Task.Yield();
no hace eso.
No hay garantía de que el rendimiento permita que cualquier trabajo se realice en el hilo principal, por el contrario, el código que llama el rendimiento a menudo tendrá prioridad sobre el trabajo de UI.
"El contexto de sincronización que está presente en un subproceso de interfaz de usuario en la mayoría de los entornos de interfaz de usuario 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 Task.Yield () para mantener una interfaz de usuario sensible "
Solo encontré Task.Yield
útil en dos escenarios:
- Pruebas unitarias, para garantizar que el código bajo prueba funcione adecuadamente en presencia de asincronía.
- Para solucionar un oscuro problema de ASP.NET donde el código de identidad no puede completarse sincrónicamente .