¿Cómo ayudará el soporte asíncrono C # 5 a los problemas de sincronización de subprocesos de la interfaz de usuario?

16

Escuché en algún lugar que C # 5 async-wait será tan increíble que no tendrás que preocuparte por hacer esto:

if (InvokeRequired)
{
    BeginInvoke(...);
    return;
}
// do your stuff here

Parece que la devolución de llamada de una operación en espera ocurrirá en el hilo original de la persona que llama. Eric Lippert y Anders Hejlsberg han afirmado varias veces que esta característica se originó a partir de la necesidad de hacer que las UI (particularmente las UI de dispositivos táctiles) sean más receptivas.

Creo que un uso común de dicha función sería algo como esto:

public class Form1 : Form
{
    // ...
    async void GetFurtherInfo()
    {
        var temperature = await GetCurrentTemperatureAsync();
        label1.Text = temperature;
    }
}

Si solo se utiliza una devolución de llamada, la configuración del texto de la etiqueta generará una excepción porque no se ejecuta en el hilo de la interfaz de usuario.

Hasta ahora no pude encontrar ningún recurso que confirme que este es el caso. ¿Alguien sabe de esto? ¿Hay algún documento que explique técnicamente cómo funcionará esto?

Proporcione un enlace de una fuente confiable, no solo responda "sí".

Alex
fuente
Esto parece altamente improbable, al menos en lo que respecta a la awaitfuncionalidad. Es solo una gran cantidad de azúcar sintáctica para el paso continuo . ¿Posiblemente hay otras mejoras no relacionadas con WinForms que se supone que ayudan? Sin embargo, eso caería dentro del marco .NET en sí, y no en C # específicamente.
Aaronaught
@Aaronaught Estoy de acuerdo, es por eso que estoy haciendo la pregunta con precisión. Edité la pregunta para aclarar de dónde vengo. Suena extraño que crearían esta característica y aún así requieren que usemos el infame estilo de código InvokeRequired.
Alex

Respuestas:

17

Creo que te estás confundiendo algunas cosas, aquí. Lo que está pidiendo ya es posible usar System.Threading.Tasks, asyncy awaiten C # 5 solo proporcionará un poco de azúcar sintáctica más agradable para la misma característica.

Usemos un ejemplo de Winforms: suelte un botón y un cuadro de texto en el formulario y use este código:

private void button1_Click(object sender, EventArgs e)
{
    Task.Factory.StartNew<int>(() => DelayedAdd(5, 10))
        .ContinueWith(t => DelayedAdd(t.Result, 20))
        .ContinueWith(t => DelayedAdd(t.Result, 30))
        .ContinueWith(t => DelayedAdd(t.Result, 50))
        .ContinueWith(t => textBox1.Text = t.Result.ToString(),
            TaskScheduler.FromCurrentSynchronizationContext());
}

private int DelayedAdd(int a, int b)
{
    Thread.Sleep(500);
    return a + b;
}

Ejecútelo y verá que (a) no bloquea el subproceso de la interfaz de usuario y (b) no obtiene el error habitual "la operación de subprocesos no es válida", a menos que elimine el TaskSchedulerargumento del último ContinueWith, en en cuyo caso lo harás.

Este es un estilo de pase de continuación estándar de pantano . La magia ocurre en la TaskSchedulerclase y específicamente en la instancia recuperada por FromCurrentSynchronizationContext. Pase esto a cualquier continuación y le dirá que la continuación debe ejecutarse en cualquier hilo llamado FromCurrentSynchronizationContextmétodo, en este caso, el hilo UI.

Los aguardadores son un poco más sofisticados en el sentido de que son conscientes de en qué hilo comenzaron y en qué hilo debe suceder la continuación. Entonces, el código anterior se puede escribir un poco más naturalmente:

private async void button1_Click(object sender, EventArgs e)
{
    int a = await DelayedAddAsync(5, 10);
    int b = await DelayedAddAsync(a, 20);
    int c = await DelayedAddAsync(b, 30);
    int d = await DelayedAddAsync(c, 50);
    textBox1.Text = d.ToString();
}

private async Task<int> DelayedAddAsync(int a, int b)
{
    Thread.Sleep(500);
    return a + b;
}

Estos dos deberían ser muy similares, y de hecho son muy similares. El DelayedAddAsyncmétodo ahora devuelve un en Task<int>lugar de un int, y por lo tanto awaitsolo está aplicando continuaciones a cada uno de ellos. La principal diferencia es que está pasando el contexto de sincronización en cada línea, por lo que no tiene que hacerlo explícitamente como lo hicimos en el último ejemplo.

En teoría, las diferencias son mucho más significativas. En el segundo ejemplo, cada línea del button1_Clickmétodo se ejecuta en el subproceso de la interfaz de usuario, pero la tarea en sí ( DelayedAddAsync) se ejecuta en segundo plano. En el primer ejemplo, todo se ejecuta en segundo plano , excepto la asignación a la textBox1.Textque nos hemos asociado explícitamente al contexto de sincronización del hilo de la interfaz de usuario.

Eso es lo que es realmente interesante await: el hecho de que un camarero puede entrar y salir del mismo método sin bloquear ninguna llamada. Usted llama await, el hilo actual vuelve a procesar los mensajes, y cuando está hecho, el camarero retomará exactamente donde lo dejó, en el mismo hilo que dejó. Pero en términos de su Invoke/ BeginInvokecontraste en la pregunta, yo ' Lamento decir que deberías haber dejado de hacerlo hace mucho tiempo.

Aaronaught
fuente
Eso es muy interesante @Aaronaught. Era consciente del estilo de paso continuo pero no era consciente de todo este "contexto de sincronización". ¿Hay algún documento que vincule este contexto de sincronización con C # 5 async-await? Entiendo que es una característica existente, pero el hecho de que la estén usando de manera predeterminada parece un gran problema, especialmente porque debe tener algunos impactos importantes en el rendimiento, ¿verdad? ¿Algún comentario adicional sobre eso? Gracias por tu respuesta por cierto.
Alex
1
@Alex: para obtener respuestas a todas esas preguntas de seguimiento, le sugiero que lea Async Performance: Understanding the Costs of Async and Await . La sección "Cuidar el contexto" explica cómo se relaciona todo con el contexto de sincronización.
Aaronaught
(Por cierto, los contextos de sincronización no son nuevos; han estado en el marco desde 2.0. El TPL los hizo mucho más fáciles de usar.)
Aaronaught
2
Me pregunto por qué todavía hay muchas discusiones sobre el uso del estilo InvokeRequired y la mayoría de los hilos que he visto ni siquiera mencionan los contextos de sincronización. Me hubiera ahorrado el tiempo de hacer esta pregunta ...
Alex
2
@ Alex: Creo que no estabas buscando en los lugares correctos . No se que decirte; Hay grandes partes de la comunidad .NET que tardan mucho en ponerse al día. Demonios, todavía veo algunos codificadores que usan la ArrayListclase en un nuevo código. Apenas tengo experiencia con RX, yo mismo. Las personas aprenden lo que necesitan saber y comparten lo que ya saben, incluso si lo que ya saben está desactualizado. Esta respuesta podría estar desactualizada en unos pocos años.
Aaronaught
4

Sí, para el subproceso de la interfaz de usuario, la devolución de llamada de una operación de espera ocurrirá en el subproceso original de la persona que llama.

Eric Lippert escribió una serie de 8 partes sobre el tema hace un año: Fabulous Adventures In Coding

EDITAR: y aquí está Anders '// build / presentation: channel9

Por cierto, ¿notó que si pone "// build /" al revés, obtendrá "/ plinq //" ;-)

Nicholas Butler
fuente