¿Cuál es la diferencia entre devolver vacío y devolver una tarea?

128

Al observar varias muestras de C # Async CTP, veo algunas funciones asíncronas que regresan voidy otras que devuelven las no genéricas Task. Puedo ver por qué devolver unTask<MyType> es útil para devolver datos al llamante cuando se completa la operación asincrónica, pero las funciones que he visto que tienen un tipo de Taskretorno nunca devuelven ningún dato. ¿Por qué no volver void?

James Cadd
fuente

Respuestas:

214

Las respuestas de SLaks y Killercam son buenas; Pensé que solo agregaría un poco más de contexto.

Su primera pregunta es esencialmente sobre qué métodos se pueden marcar async.

Un método marcado como asyncpuede volver void, Tasko Task<T>. Cuáles son las diferencias entre ellos?

Se Task<T>puede esperar un método asíncrono de retorno, y cuando la tarea se complete, ofrecerá una T.

Se Taskpuede esperar un método asíncrono de retorno, y cuando la tarea se completa, la continuación de la tarea está programada para ejecutarse.

UNA void método asíncrono de volver no puede ser esperada; Es un método de "dispara y olvida". Funciona de forma asíncrona, y no tienes forma de saber cuándo se hace. Esto es más que un poco extraño; como dice SLaks, normalmente solo lo haría al hacer un controlador de eventos asíncrono. El evento se dispara, el controlador se ejecuta; nadie va a "esperar" la tarea devuelta por el controlador de eventos porque los controladores de eventos no devuelven tareas, e incluso si lo hicieran, ¿qué código usaría la tarea para algo? En general, no es el código de usuario el que transfiere el control al controlador en primer lugar.

Su segunda pregunta, en un comentario, es esencialmente sobre lo que se puede awaiteditar:

¿Qué tipo de métodos se pueden awaiteditar? ¿Se puede editar un método de retorno de vacío await?

No, no se puede esperar un método de retorno nulo. El compilador se traduce await M()en una llamada a M().GetAwaiter(), donde GetAwaiterpodría ser un método de instancia o un método de extensión. El valor esperado debe ser uno para el que pueda obtener un camarero; claramente, un método de retorno de vacío no produce un valor del cual se pueda obtener un camarero.

Task-los métodos de retorno pueden producir valores esperados. Anticipamos que terceros querrán crear sus propias implementaciones de Taskobjetos similares que se pueden esperar, y usted podrá esperarlos. Sin embargo, no se le permitirá declarar asyncmétodos que devuelvan cualquier cosa que no sea void, Tasko Task<T>.

(ACTUALIZACIÓN: Mi última oración allí puede ser falsificada por una versión futura de C #; hay una propuesta para permitir tipos de retorno que no sean tipos de tareas para métodos asíncronos).

(ACTUALIZACIÓN: La función mencionada anteriormente llegó a C # 7.)

Eric Lippert
fuente
77
+1 Creo que lo único que falta es la diferencia en cómo se tratan las excepciones en los métodos asíncronos de retorno nulo.
João Angelo
10
@JamesCadd: Supongamos que un trabajo asincrónico arroja una excepción. ¿Quién lo atrapa? El código que inició la tarea asincrónica ya no está en la pila, puede que ni siquiera esté en el mismo hilo , y las excepciones suponen que todos los bloques catch / finally están en la pila . Entonces, ¿Qué haces? Almacenamos la información de excepción en la Tarea, para que pueda inspeccionarla más tarde. Pero si el método no devuelve, entonces no hay Tarea disponible para el código de usuario. Cómo lidiamos exactamente con esa situación ha sido motivo de cierta controversia y no recuerdo en este momento lo que decidimos.
Eric Lippert el
8
Realmente le hice esta pregunta a Stephen Toub en BUILD. En .NET 4.0, las excepciones no observadas y no controladas en Tareas eventualmente bloquearían el proceso una vez que TPL detecte que no se observaron. En 4.5 han cambiado el comportamiento predeterminado para que las excepciones no observadas se sigan informando a través del evento TaskScheduler :: UnobservedTaskException, pero ya no bloqueará el proceso. Si desea el antiguo comportamiento 4.0, puede volver a activarlo con <runtime> <ThrowUnobservedTaskExceptions enabled = "true" /> </runtime>. Lo más probable es que el cambio se haya realizado precisamente para admitir disparar y olvidar para métodos asíncronos nulos.
Drew Marsh
44
async voidLos métodos plantean su excepción sobre el estado SynchronizationContextactivo en el momento en que comenzaron a ejecutarse. Esto es similar al comportamiento de los controladores de eventos (sincrónicos). @DrewMarsh: la UnobservedTaskExceptionconfiguración y el tiempo de ejecución solo se aplican a los métodos de tareas asíncronas "disparar y olvidar" , no a los async voidmétodos.
Stephen Cleary
1
Enlace de cita para información de manejo de excepciones asíncronas: blogs.msdn.com/b/pfxteam/archive/2012/04/12/10293335.aspx#11
Luke Puplett
23

En caso de que la persona que llama quiera esperar la tarea o agregar una continuación.

De hecho, la única razón para regresar voides si no puede regresar Taskporque está escribiendo un controlador de eventos.

SLaks
fuente
Pensé que también era posible esperar métodos que devolvieran un tipo de vacío: ¿podría explicar un poco?
James Cadd
1
No puedes. Si el método regresa void, no tiene forma de llegar a la tarea que genera. (En realidad, no estoy seguro de si incluso genera un Taskabsoluto)
SLaks
18

Los métodos regresan Tasky Task<T>son componibles, lo que significa que puede awaitusarlos dentro de un asyncmétodo.

asyncLos métodos de retorno voidno son componibles, pero tienen otras dos propiedades importantes:

  1. Se pueden usar como controladores de eventos.
  2. Representan una operación asincrónica de "nivel superior".

El segundo punto es importante cuando se trata de un contexto que mantiene un recuento de operaciones asincrónicas pendientes.

El contexto ASP.NET es uno de esos contextos; Si utiliza Taskmétodos asincrónicos sin esperarlos de un voidmétodo asincrónico , la solicitud de ASP.NET se completará demasiado pronto.

Otro contexto es el AsyncContextque escribí para pruebas unitarias (disponible aquí ): elAsyncContext.Run método rastrea el recuento de operaciones pendientes y regresa cuando es cero.

Stephen Cleary
fuente
12

Tipo Task<T>es el tipo de caballo de batalla de la Biblioteca Paralela de Tareas (TPL), representa el concepto de "algún trabajo / trabajo que va a producir un resultado de tipoT en el futuro". El concepto de "trabajo que se completará en el futuro pero no devuelve ningún resultado" está representado por el tipo de tarea no genérico.

Precisamente cómo se producirá el resultado del tipo Ty los detalles de implementación de una tarea en particular; el trabajo puede asignarse a otro proceso en la máquina local, a otro subproceso, etc. Las tareas TPL generalmente se asignan a subprocesos de trabajo desde un grupo de subprocesos en el proceso actual, pero ese detalle de implementación no es fundamental para el Task<T>tipo; más bien un Task<T>puede representar cualquier operación de alta latencia que produce unT .

Según su comentario anterior:

La awaitexpresión significa "evaluar esta expresión para obtener un objeto que represente el trabajo que producirá un resultado en el futuro. Registre el resto del método actual como la devolución de llamada asociada con la continuación de esa tarea. Una vez que se produce esa tarea y se devuelve la llamada está registrado, devuelva inmediatamente el control a mi interlocutor ". Esto es opuesto / en contraste con una llamada de método regular, lo que significa "recuerda lo que estás haciendo, ejecuta este método hasta que esté completamente terminado y luego retoma donde lo dejaste, ahora conociendo el resultado del método".


Editar: Debería citar el artículo de Eric Lippert en la revista MSDN de octubre de 2011, ya que esto fue de gran ayuda para comprender esto en primer lugar.

Para más información y páginas blancas, vea aquí .

Espero que esto sea de ayuda.

Caballero de la Luna
fuente