¿Tienes que poner Task.Run en un método para hacerlo asíncrono?

304

Estoy tratando de entender async esperar en la forma más simple. Quiero crear un método muy simple que agregue dos números por el bien de este ejemplo, concedido, no es tiempo de procesamiento, solo es cuestión de formular un ejemplo aquí.

Ejemplo 1

private async Task DoWork1Async()
{
    int result = 1 + 2;
}

Ejemplo 2

private async Task DoWork2Async()
{
    Task.Run( () =>
    {
        int result = 1 + 2;
    });
}

Si espero, ¿ DoWork1Async()se ejecutará el código de forma síncrona o asíncrona?

¿Debo envolver el código de sincronización Task.Runpara hacer que el método sea esperable Y asíncrono para no bloquear el hilo de la interfaz de usuario?

Estoy tratando de averiguar si mi método es Tasko devuelve, Task<T>¿tengo que ajustar el código Task.Runpara hacerlo asíncrono?

Estúpida pregunta, estoy seguro, pero veo ejemplos en la red donde la gente espera un código que no tiene nada asíncrono y no está envuelto en un Task.Runo StartNew.

Neal
fuente
30
¿Tu primer fragmento no te da ninguna advertencia?
svick

Respuestas:

587

Primero, aclaremos algo de terminología: "asíncrono" ( async) significa que puede ceder el control al hilo de llamada antes de que comience. En un asyncmétodo, esos puntos de "rendimiento" son awaitexpresiones.

Esto es muy diferente al término "asíncrono", ya que (mal) usado por la documentación de MSDN durante años significa "se ejecuta en un hilo de fondo".

Para confundir aún más el tema, asynces muy diferente de "espera"; Hay algunos asyncmétodos cuyos tipos de retorno no son esperados, y muchos métodos que devuelven tipos que no lo son async.

Suficiente sobre lo que no son ; esto es lo que son :

  • La asyncpalabra clave permite un método asincrónico (es decir, permite awaitexpresiones). asynclos métodos pueden regresar Task, Task<T>o (si debe hacerlo) void.
  • Cualquier tipo que siga un cierto patrón puede estar a la espera. Los tipos de espera más comunes son Tasky Task<T>.

Entonces, si reformulamos su pregunta a "¿cómo puedo ejecutar una operación en un subproceso en segundo plano de una manera que sea esperable", la respuesta es usar Task.Run:

private Task<int> DoWorkAsync() // No async because the method does not need await
{
  return Task.Run(() =>
  {
    return 1 + 2;
  });
}

(Pero este patrón es un enfoque pobre; ver más abajo).

Pero si su pregunta es "¿cómo creo un asyncmétodo que pueda ceder el paso a su interlocutor en lugar de bloquearlo", la respuesta es declarar el método asyncy usarlo awaitpara sus puntos de "ceder":

private async Task<int> GetWebPageHtmlSizeAsync()
{
  var client = new HttpClient();
  var html = await client.GetAsync("http://www.example.com/");
  return html.Length;
}

Por lo tanto, el patrón básico de las cosas es que el asynccódigo dependa de "esperables" en sus awaitexpresiones. Estos "esperables" pueden ser otros asyncmétodos o simplemente métodos regulares que devuelven objetos esperables. Métodos habituales que regresan Task/ Task<T> pueden utilizar Task.Runpara ejecutar código en un subproceso en segundo plano, o (más comúnmente) que pueden utilizar TaskCompletionSource<T>o uno de sus accesos directos ( TaskFactory.FromAsync, Task.FromResult, etc.). Yo no recomiendo el embalaje de un método completo en Task.Run; los métodos síncronos deben tener firmas síncronas, y debe dejarse en manos del consumidor si debe estar envuelto en un Task.Run:

private int DoWork()
{
  return 1 + 2;
}

private void MoreSynchronousProcessing()
{
  // Execute it directly (synchronously), since we are also a synchronous method.
  var result = DoWork();
  ...
}

private async Task DoVariousThingsFromTheUIThreadAsync()
{
  // I have a bunch of async work to do, and I am executed on the UI thread.
  var result = await Task.Run(() => DoWork());
  ...
}

Tengo una async/ awaitintroducción en mi blog; al final hay algunos buenos recursos de seguimiento. Los documentos de MSDN para también asyncson inusualmente buenos.

Stephen Cleary
fuente
8
@sgnsajgon: Sí. asyncmétodos deben devolver Task, Task<T>o void. Tasky Task<T>están a la espera; voidno es.
Stephen Cleary
3
En realidad, async voidse compilará una firma de método, es una idea bastante terrible, ya que pierde el puntero a su tarea asíncrona
IEatBagels
44
@TopinFrassi: Sí, compilarán, pero voidno está a la espera.
Stephen Cleary
44
@ohadinho: No, de lo que estoy hablando en la publicación del blog es cuando todo el método es solo una llamada Task.Run(como DoWorkAsyncen esta respuesta). Usar Task.Runpara llamar a un método desde un contexto de IU es apropiado (como DoVariousThingsFromTheUIThreadAsync).
Stephen Cleary
2
Sí exactamente. Es válido usarlo Task.Runpara invocar un método, pero si hay una parte de Task.Runtodo (o casi todo) el código del método, entonces ese es un antipatrón; solo mantenga ese método sincronizado y Task.Runsuba el nivel.
Stephen Cleary
22

Una de las cosas más importantes para recordar al decorar un método con asíncrono es que al menos hay un operador en espera dentro del método. En su ejemplo, lo traduciría como se muestra a continuación usando TaskCompletionSource .

private Task<int> DoWorkAsync()
{
    //create a task completion source
    //the type of the result value must be the same
    //as the type in the returning Task
    TaskCompletionSource<int> tcs = new TaskCompletionSource<int>();
    Task.Run(() =>
    {
        int result = 1 + 2;
        //set the result to TaskCompletionSource
        tcs.SetResult(result);
    });
    //return the Task
    return tcs.Task;
}

private async void DoWork()
{
    int result = await DoWorkAsync();
}
Ronald Ramos
fuente
26
¿Por qué utiliza TaskCompletionSource, en lugar de simplemente devolver la tarea devuelta por el método Task.Run () (y cambiar su cuerpo para devolver el resultado)?
irónico
44
Solo una nota al margen. Un método que tiene una firma de "vacío asíncrono" es generalmente una mala práctica y se considera un código incorrecto, ya que puede provocar un punto muerto en la interfaz de usuario con bastante facilidad. La excepción principal son los controladores de eventos asíncronos.
Jazzeroki
12

Cuando usa Task.Run para ejecutar un método, Task obtiene un hilo de threadpool para ejecutar ese método. Desde la perspectiva del subproceso de la interfaz de usuario, es "asíncrono" ya que no bloquea el subproceso de la interfaz de usuario. Esto está bien para la aplicación de escritorio, ya que generalmente no necesita muchos subprocesos para ocuparse de las interacciones del usuario.

Sin embargo, para la aplicación web cada solicitud es atendida por un subproceso de grupo de subprocesos y, por lo tanto, el número de solicitudes activas se puede aumentar guardando dichos subprocesos. El uso frecuente de hilos de agrupación de hilos para simular la operación asincrónica no es escalable para aplicaciones web.

True Async no implica necesariamente el uso de un subproceso para operaciones de E / S, como el acceso a archivos / bases de datos, etc. Puede leer esto para comprender por qué la operación de E / S no necesita subprocesos. http://blog.stephencleary.com/2013/11/there-is-no-thread.html

En su ejemplo simple, es un cálculo puro vinculado a la CPU, por lo que usar Task.Run está bien.

zheng yu
fuente
Entonces, si tengo que consumir una api externa síncrona dentro de un controlador de api web, ¿NO debería envolver la llamada síncrona en Task.Run ()? Como dijiste, hacerlo mantendrá desbloqueado el subproceso de solicitud inicial, pero está usando otro subproceso de grupo para llamar a la API externa. De hecho, creo que sigue siendo una buena idea porque, de esta manera, en teoría puede usar dos hilos de grupo para procesar muchas solicitudes, por ejemplo, un hilo puede procesar muchas solicitudes entrantes y otro puede llamar a la API externa para todas estas solicitudes.
stt106
Estoy de acuerdo No estoy diciendo que no debas envolver absolutamente todas las llamadas síncronas dentro de Task.Run (). Simplemente estoy señalando un problema potencial.
zheng yu
1
@ stt106 I should NOT wrap the synchronous call in Task.Run()eso es correcto. Si lo haces, solo estarías cambiando los hilos. es decir, está desbloqueando el subproceso de solicitud inicial pero está tomando otro subproceso del conjunto de subprocesos que podría haberse utilizado para procesar otra solicitud. El único resultado es una sobrecarga de cambio de contexto cuando la llamada se completa para una ganancia absolutamente cero
Saeb Amini