Async-aguardar problema en EF 6

async-await entity-framework entity-framework-6 wpf

Pregunta

Estoy probando la programación async-await con entidad-framework 6 (código primero) + WPF y no puedo ver por qué la interfaz de usuario aún se congela después de que hice el código asíncrono. Esto es lo que estoy haciendo desde la primera línea:

Primero hay un controlador de eventos que responde a un botón de clic:

private async void LoginButton_Click(object sender, RoutedEventArgs e) {
  if (await this._service.Authenticate(username.Text, password.Password) != null)
    this.Close();
}

Luego tengo el método de autenticación en mi capa de servicio:

private async void LoginButton_Click(object sender, RoutedEventArgs e) {
  if (await this._service.Authenticate(username.Text, password.Password) != null)
    this.Close();
}

y al final está el código EF en el contexto:

private async void LoginButton_Click(object sender, RoutedEventArgs e) {
  if (await this._service.Authenticate(username.Text, password.Password) != null)
    this.Close();
}

Actualización: después de un seguimiento, la causa de la congelación de la interfaz de usuario resultó ser el proceso de inicialización. El subproceso de la IU se bloquea hasta que el contexto de EF se inicializa y una vez que se realiza, el proceso real de consulta / guardado se realiza de forma asíncrona.

Actualice el resultado de la depuración después de la llamada en Task.Yield () al comienzo del controlador de clic:

private async void LoginButton_Click(object sender, RoutedEventArgs e) {
  if (await this._service.Authenticate(username.Text, password.Password) != null)
    this.Close();
}

Respuesta aceptada

Los métodos marcados como "asíncronos" siguen siendo sincrónicos hasta el momento en que ocurre la primera "espera". Debido a eso, si algo de lo que sucede en ese código inicial toma demasiado tiempo (200 ms o más, creo que es la pauta para WinRT, lo que parece razonable), es posible que desee forzar el código para que devuelva más rápido insertando una espera anterior.

Por ejemplo, en su LoginButton_Click, puede insertar una primera línea de ' await Task.Yield ()' que permitirá que la llamada regrese más rápido al hilo de la interfaz de usuario.

Ahora, solo con ese cambio, todos los métodos se seguirán ejecutando en el subproceso de la interfaz de usuario debido al comportamiento de async / await. Todavía me gusta hacer ese cambio primero porque en muchos casos es lo que el usuario realmente espera que esté sucediendo (el modificador 'asíncrono' es algo confuso en ese sentido), y es algo que puede hacer al inicio de un controlador sin tener que desordenar con cosas más abajo en la pila.

El siguiente paso que podemos hacer si lo anterior es insuficiente (como la inicialización del contexto está demorando demasiado, aún ocurre en el hilo de la interfaz de usuario y aún congela la interfaz de usuario, en un punto ligeramente diferente en el tiempo) es tomar las partes que no lo hacen. No es necesario que ocurra en el subproceso de la interfaz de usuario y que espere a que sepan que se pueden procesar en cualquier subproceso, no solo el subproceso de la interfaz de usuario. En general, esta es una buena práctica para la capacidad de respuesta, incluso en escenarios donde el código actualmente se ejecuta "lo suficientemente rápido" para que no sea un problema notable.

Para eso, usamos agregar ConfigureAwait (falso) a la Tarea.

  • El método GetUserAsync debería agregarlo ("encadenarlo") después de la llamada FirstOrDefaultAsync
    • alternativamente, e IMHO un poco más limpio, es simplemente deshacerse de las palabras clave async / await en el método GetUserAsync y simplemente devolver la tarea que regresó de FirstOrDefaultAsync. El async / await no es realmente "comprando" nada en este método como está, IMHO :)
  • en Autenticar, probablemente debería agregarlo después de la llamada GetUserAsync
    • El único 'gotcha' potencial aquí del cual no estoy seguro es si el CurrentUser está enlazado a la UI. Dado que es un miembro del _servicio, supongo que no lo es, pero incluso si lo es, creo que WPF está bien con los elementos enlazados a datos que se actualizan en subprocesos que no pertenecen a la interfaz de usuario y se trata de volver a calcular los cambios en la interfaz de usuario (¿despachador?) hilo. Esto es diferente a los marcos como Silverlight, donde la actualización de una propiedad en una IU no vinculada a los datos en la UI causará el mismo error de subprocesos como si hubiera actualizado manualmente el control de destino. Si me equivoco al respecto, y 1) el CurrentUser está enlazado a datos en su UI y 2) esa actualización enlazada a los datos en un subproceso que no es de UI causa una excepción de tiempo de ejecución, luego evite la adición de ConfigureAwait (falso) en este método . Perdón por la duración de esto, solo trato de relacionar lo que estoy un poco inseguro de esta modificación en particular. :)
  • en LoginButton_Click, NO deberíamos agregarlo, porque el resto del método (this.Close) debe suceder en el subproceso de la interfaz de usuario, y ConfigureAwait (falso) aquí rompería eso

Una vez que ambos cambios estén en marcha, ambos 1) devolverán el control a la persona que llama lo más rápido posible (haciendo la menor cantidad de código síncrono al controlador de eventos) y 2) realizaremos un trabajo que no es necesario estar en el subproceso de la interfaz de usuario en otros subprocesos, lo que se espera que signifique que su interfaz de usuario ya no se "congele".

Si aún se congela después de esos cambios, es posible que deba ejecutarlo bajo un depurador, y cuando se congela, haga una pausa para ver cuál es la pila del subproceso de la interfaz de usuario para encontrar el código ofensivo. :)

¡Buena suerte!




Licencia bajo: CC-BY-SA with attribution
No afiliado con Stack Overflow
¿Es esto KB legal? Sí, aprende por qué
Licencia bajo: CC-BY-SA with attribution
No afiliado con Stack Overflow
¿Es esto KB legal? Sí, aprende por qué