¿Cuándo usaría Task.Yield ()?

218

Estoy usando async / await y Taskmucho, pero nunca he estado usando Task.Yield()y, para ser honesto, incluso con todas las explicaciones, no entiendo por qué necesitaría este método.

¿Alguien puede dar un buen ejemplo donde Yield()se requiere?

Krumelur
fuente

Respuestas:

241

Cuando usa async/ await, no hay garantía de que el método al que llama cuando lo haga await FooAsync()realmente se ejecute de forma asincrónica. La implementación interna es libre de regresar utilizando una ruta completamente sincrónica.

Si está haciendo una API donde es crítico que no bloquee y ejecute algún código de forma asincrónica, y existe la posibilidad de que el método llamado se ejecute sincrónicamente (bloqueo efectivo), el uso await Task.Yield()obligará a su método a ser asíncrono y devolverá control en ese punto. El resto del código se ejecutará más adelante (en ese momento, aún puede ejecutarse sincrónicamente) en el contexto actual.

Esto también puede ser útil si realiza un método asincrónico que requiere una inicialización de "ejecución prolongada", es decir:

 private async void button_Click(object sender, EventArgs e)
 {
      await Task.Yield(); // Make us async right away

      var data = ExecuteFooOnUIThread(); // This will run on the UI thread at some point later

      await UseDataAsync(data);
 }

Sin la Task.Yield()llamada, el método se ejecutará sincrónicamente hasta la primera llamada a await.

Reed Copsey
fuente
26
Siento que estoy malinterpretando algo aquí. Si await Task.Yield()obliga al método a ser asíncrono, ¿por qué nos molestaría escribir código asincrónico "real"? Imagine un método de sincronización pesado. Para hacerlo asíncrono, simplemente agregue asyncy await Task.Yield()al principio y mágicamente, ¿será asíncrono? Eso sería más o menos como envolver todo el código de sincronización Task.Run()y crear un método asíncrono falso.
Krumelur
14
@ Krumelur Hay una gran diferencia: mira mi ejemplo. Si usa a Task.Runpara implementarlo, ExecuteFooOnUIThreadse ejecutará en el grupo de subprocesos, no en el subproceso de la interfaz de usuario. Con await Task.Yield(), lo fuerza a ser asíncrono de manera que el código posterior todavía se ejecute en el contexto actual (solo en un momento posterior). No es algo que normalmente haría, pero es bueno que exista la opción si es necesario por alguna extraña razón.
Reed Copsey
77
Una pregunta más: si ExecuteFooOnUIThread()durara mucho tiempo, aún bloquearía el hilo de la interfaz de usuario durante un tiempo prolongado en algún momento y haría que la interfaz de usuario no respondiera, ¿es correcto?
Krumelur
77
@ Krumelur Sí, lo haría. Simplemente no de inmediato, sucedería más tarde.
Reed Copsey
33
Aunque esta respuesta es técnicamente correcta, la afirmación de que "el resto del código se ejecutará más adelante" es demasiado abstracta y puede ser engañosa. El cronograma de ejecución del código después de Task.Yield () depende mucho de SynchronisationContext concreto. Y la documentación de MSDN establece claramente que "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 por encima del trabajo de entrada y representación. Por esta razón, no confíe en la tarea Task.Yield () en espera. ; para mantener una interfaz de usuario receptiva ".
Vitaliy Tsvayer
36

Internamente, await Task.Yield()simplemente pone en cola la continuación en el contexto de sincronización actual o en un subproceso de grupo aleatorio, si SynchronizationContext.Currentes así null.

Se implementa eficientemente como camarero personalizado. Un código menos eficiente que produzca el mismo efecto podría ser tan simple como esto:

var tcs = new TaskCompletionSource<bool>();
var sc = SynchronizationContext.Current;
if (sc != null)
    sc.Post(_ => tcs.SetResult(true), null);
else
    ThreadPool.QueueUserWorkItem(_ => tcs.SetResult(true));
await tcs.Task;

Task.Yield()se puede usar como atajo para algunas alteraciones extrañas del flujo de ejecución. Por ejemplo:

async Task DoDialogAsync()
{
    var dialog = new Form();

    Func<Task> showAsync = async () => 
    {
        await Task.Yield();
        dialog.ShowDialog();
    }

    var dialogTask = showAsync();
    await Task.Yield();

    // now we're on the dialog's nested message loop started by dialog.ShowDialog 
    MessageBox.Show("The dialog is visible, click OK to close");
    dialog.Close();

    await dialogTask;
    // we're back to the main message loop  
}

Dicho esto, no puedo pensar en ningún caso en el Task.Yield()que no se pueda reemplazar con Task.Factory.StartNewel programador de tareas adecuado.

Ver también:

nariz
fuente
En su ejemplo, ¿cuál es la diferencia entre lo que hay allí y var dialogTask = await showAsync();?
Erik Philips
@ErikPhilips, var dialogTask = await showAsync()no se compilará porque la await showAsync()expresión no devuelve un Task(a diferencia de lo que ocurre sin await). Dicho esto, si lo hace await showAsync(), la ejecución después se reanudará solo después de que se haya cerrado el diálogo, así es como es diferente. Eso es porque window.ShowDialoges una API síncrona (a pesar de que todavía bombea mensajes). En ese código, quería continuar mientras todavía se muestra el diálogo.
noseratio
5

Un uso de Task.Yield()es evitar un desbordamiento de la pila al hacer una recursión asíncrona. Task.Yield()evita la continuación sincrónica. Sin embargo, tenga en cuenta que esto puede causar una excepción OutOfMemory (como lo señaló Triynko). La recursión sin fin todavía no es segura y probablemente sea mejor reescribir la recursión como un bucle.

private static void Main()
    {
        RecursiveMethod().Wait();
    }

    private static async Task RecursiveMethod()
    {
        await Task.Delay(1);
        //await Task.Yield(); // Uncomment this line to prevent stackoverlfow.
        await RecursiveMethod();
    }
Joakim MH
fuente
44
Esto podría evitar un desbordamiento de la pila, pero eventualmente se quedará sin memoria del sistema si lo deja correr el tiempo suficiente. Cada iteración crearía una nueva Tarea que nunca se completa, ya que la Tarea externa está esperando una Tarea interna, que está esperando otra Tarea interna, y así sucesivamente. Esto no esta bien. Alternativamente, podría simplemente tener una Tarea más externa que nunca se complete, y simplemente hacer que se repita. La tarea nunca se completaría, pero solo habría uno de ellos. Dentro del bucle, podría ceder o esperar cualquier cosa que desee.
Triynko
No puedo reproducir el desbordamiento de la pila. Parece que await Task.Delay(1)es suficiente para prevenirlo. (Aplicación de consola, .NET Core 3.1, C # 8)
Theodor Zoulias
-8

Task.Yield() puede usarse en implementaciones simuladas de métodos asincrónicos.

mhsirig
fuente
44
Debes proporcionar algunos detalles.
PJProudhon
3
Para este propósito, prefiero usar Task.CompletedTask ; consulte la sección Task.CompletedTask en esta publicación de blog de msdn para obtener más información.
Grzegorz Smulko
2
El problema con el uso de Task.CompletedTask o Task.FromResult es que puede perder errores que solo aparecen cuando el método se ejecuta de forma asincrónica.
Joakim MH