c# - No esperar una llamada asincrónica sigue siendo asincrónica, ¿verdad?




.net asynchronous (2)

Lo siento si esta es una pregunta tonta (o un duplicado).

Tengo una función A :

public async Task<int> A(/* some parameters */)
{
    var result = await SomeOtherFuncAsync(/* some other parameters */);

    return (result);
}

Tengo otra función B , que llama a A pero no usa el valor de retorno:

public Task B(/* some parameters */)
{
    var taskA = A(/* parameters */); // #1

    return (taskA);
}

Tenga en cuenta que B no se declara async y no está esperando la llamada a A La llamada a A no es una llamada de disparar y olvidar: B llama a B así:

public async Task C()
{
    await B(/* parameters */);
}

Tenga en cuenta que en el n . ° 1 , no hay que await . Tengo un compañero de trabajo que afirma que esto hace que la llamada a A síncrona y sigue apareciendo con Console.WriteLine logs que aparentemente demuestran su punto.

Intenté señalar que solo porque no esperamos los resultados dentro de B , se espera la cadena de tareas y la naturaleza del código dentro de A no cambia solo porque no lo esperamos. Como el valor de retorno de A no es necesario, no es necesario esperar la tarea en el sitio de la llamada, siempre y cuando alguien de la cadena lo espere (lo que sucede en C ).

Mi compañero de trabajo es muy insistente y comencé a dudar de mí mismo. ¿Está mal mi entendimiento?


Lo siento si esta es una pregunta tonta

No es una pregunta tonta. Es una pregunta importante.

Tengo un compañero de trabajo que afirma que esto hace que la llamada a A sea síncrona y sigue apareciendo con Console.WriteLine logs que aparentemente demuestran su punto.

Ese es el problema fundamental allí mismo, y debe educar a su compañero de trabajo para que dejen de engañarse a sí mismos y a los demás. No hay tal cosa como una llamada asincrónica . La llamada no es lo que es asíncrono, nunca . Dilo conmigo. Las llamadas no son asíncronas en C # . En C #, cuando llama a una función, esa función se llama inmediatamente después de calcular todos los argumentos .

Si su compañero de trabajo o usted cree que existe una llamada asincrónica, se encontrará con un mundo de dolor porque sus creencias sobre cómo funciona la asincronía estarán muy desconectadas de la realidad.

Entonces, ¿es correcto tu compañero de trabajo? Por supuesto que lo son. La llamada a A es síncrona porque todas las llamadas a funciones son síncronas . Pero el hecho de que crean que existe una "llamada asincrónica" significa que están muy equivocados acerca de cómo funciona la asincronía en C #.

Si específicamente su compañero de trabajo cree que await M() alguna manera hace que la llamada a M() "asíncrona", entonces su compañero de trabajo tiene un gran malentendido. await es un operador . Es un operador complicado, sin duda, pero es un operador y funciona con valores. await M() y var t = M(); await t; var t = M(); await t; Son lo mismo . La espera ocurre después de la llamada porque la await opera en el valor que se devuelve . await NO es una instrucción para el compilador de "generar una llamada asincrónica a M ()" o algo así; no hay tal cosa como una "llamada asincrónica".

Si esa es la naturaleza de su falsa creencia, entonces tienes la oportunidad de educar a tu compañero de trabajo sobre lo await significa await . await significa algo simple pero poderoso. Significa:

  • Mira la Task en la que estoy operando.
  • Si la tarea se completa excepcionalmente, arroje esa excepción
  • Si la tarea se completa normalmente, extraiga ese valor y úselo
  • Si la tarea está incompleta, registre el resto de este método como la continuación de la tarea esperada y devuelva una nueva Task represente el flujo de trabajo asincrónico incompleto de esta llamada a mi interlocutor .

Eso es todo lo que await . Simplemente examina el contenido de una tarea, y si la tarea está incompleta, dice "bueno, no podemos avanzar en este flujo de trabajo hasta que se complete esa tarea, así que regrese a mi interlocutor que encontrará algo más para esta CPU que hacer".

La naturaleza del código dentro de A no cambia solo porque no lo esperamos.

Eso es correcto. Llamamos sincrónicamente A , y devuelve una Task . El código después del sitio de la llamada no se ejecuta hasta que regrese A Lo interesante de A es que A puede devolver una Task incompleta a su llamante , y esa tarea representa un nodo en un flujo de trabajo asincrónico . El flujo de trabajo ya es asíncrono y, como observa, no importa a A lo que haga con su valor de retorno después de que regrese; A no tiene idea de si va a await la Task devuelta o no. A ejecuta todo el tiempo que puede, y luego devuelve una tarea completada normalmente, o una tarea completada excepcionalmente, o devuelve una tarea incompleta. Pero nada de lo que haga en el sitio de llamadas cambia eso.

Como el valor de retorno de A no es necesario, no es necesario esperar la tarea en el sitio de la llamada

Correcto.

no hay necesidad de esperar la tarea en el sitio de la llamada, siempre y cuando alguien de la cadena lo espere (lo que sucede en C).

Ahora me has perdido. ¿Por qué alguien tiene que esperar la Task devuelta por A ? Diga por qué cree que se requiere que alguien await esa Task , porque podría tener una creencia falsa.

Mi compañero de trabajo es muy insistente y comencé a dudar de mí mismo. ¿Está mal mi entendimiento?

Su compañero de trabajo es casi seguro que está equivocado. Su análisis parece correcto hasta el punto en que dice que hay un requisito de que se await cada Task , lo cual no es cierto. Es extraño no await una Task porque significa que escribiste un programa donde comenzaste una operación y no te importa cuándo o cómo se completa, y ciertamente huele mal escribir un programa como ese, pero no hay un requisito para await cada Task . Si crees que existe, de nuevo, di cuál es esa creencia y lo resolveremos.


Tienes razón. Crear una tarea solo hace eso y no le importa cuándo y quién esperará su resultado. Intenta poner a la await Task.Delay(veryBigNumber); en SomeOtherFuncAsync y la salida de la consola debería ser lo que esperarías.

Esto se llama elidir y le sugiero que lea esta publicación de blog, donde puede ver por qué debería o no hacer tal cosa.

También algún ejemplo mínimo (poco complicado) copiando su código demostrando que tiene razón:

class Program
    {
        static async Task Main(string[] args)
        {
            Console.WriteLine($"Start of main {Thread.CurrentThread.ManagedThreadId}");
            var task = First();
            Console.WriteLine($"Middle of main {Thread.CurrentThread.ManagedThreadId}");
            await task;
            Console.WriteLine($"End of main {Thread.CurrentThread.ManagedThreadId}");
        }

        static Task First()
        {
            return SecondAsync();
        }

        static async Task SecondAsync()
        {
            await ThirdAsync();
        }

        static async Task ThirdAsync()
        {
            Console.WriteLine($"Start of third {Thread.CurrentThread.ManagedThreadId}");
            await Task.Delay(1000);
            Console.WriteLine($"End of third {Thread.CurrentThread.ManagedThreadId}");
        }
    }

Esto escribe Middle of main antes de End of third , demostrando que de hecho es asíncrono. Además, puede (muy probablemente) ver que los extremos de las funciones se ejecutan en un hilo diferente que el resto del programa. Tanto los inicios como el medio de main siempre se ejecutarán en el mismo subproceso porque de hecho son sincrónicos (main comienza, llama a la cadena de funciones, terceros regresa (puede regresar en la línea con la palabra clave await ) y luego main continúa como si hubiera nunca estuvo involucrada una función asincrónica. Las terminaciones después de las palabras clave en await en ambas funciones pueden ejecutarse en cualquier subproceso en el ThreadPool (o en el contexto de sincronización que está utilizando).

Ahora es interesante notar que si Task.Delay en Third no tomó mucho tiempo y realmente terminó sincrónicamente, todo esto se ejecutaría en un solo hilo. Lo que es más, a pesar de que se ejecutaría de forma asincrónica, podría ejecutarse en un solo hilo. No existe una regla que establezca que una función asíncrona usará más de un hilo, es muy posible que solo haga algún otro trabajo mientras espera que termine alguna tarea de E / S.





async-await