No se puede especificar el modificador 'asíncrono' en el método 'Principal' de una aplicación de consola

445

Soy nuevo en la programación asincrónica con el asyncmodificador. Estoy tratando de descubrir cómo asegurarme de que mi Mainmétodo de una aplicación de consola realmente se ejecute de forma asincrónica.

class Program
{
    static void Main(string[] args)
    {
        Bootstrapper bs = new Bootstrapper();
        var list = bs.GetList();
    }
}

public class Bootstrapper {

    public async Task<List<TvChannel>> GetList()
    {
        GetPrograms pro = new GetPrograms();

        return await pro.DownloadTvChannels();
    }
}

Sé que esto no se ejecuta de forma asincrónica desde "la parte superior". Como no es posible especificar el asyncmodificador en el Mainmétodo, ¿cómo puedo ejecutar el código de mainforma asincrónica?

danielovich
fuente
23
Este ya no es el caso en C # 7.1. Los métodos principales pueden ser asíncronos
Vasily Sliounaiev
2
Aquí está el anuncio de la publicación del blog C # 7.1 . Vea la sección titulada Async Main .
Styfle

Respuestas:

382

Como descubrió, en VS11 el compilador no permitirá un async Mainmétodo. Esto fue permitido (pero nunca recomendado) en VS2010 con el Async CTP.

Tengo publicaciones de blog recientes sobre programas de consola asíncronos / en espera y asíncronos en particular. Aquí hay información de fondo de la publicación de introducción:

Si "esperar" ve que lo esperable no se ha completado, entonces actúa de forma asíncrona. Le dice a la persona que espera ejecutar el resto del método cuando se complete, y luego regresa del método asíncrono. Await también capturará el contexto actual cuando pase el resto del método a lo esperado.

Más adelante, cuando se complete lo esperado, ejecutará el resto del método asíncrono (dentro del contexto capturado).

He aquí por qué este es un problema en los programas de consola con un async Main:

Recuerde de nuestra publicación de introducción que un método asincrónico volverá a la persona que llama antes de que se complete. Esto funciona perfectamente en aplicaciones de UI (el método solo regresa al bucle de eventos de UI) y en aplicaciones ASP.NET (el método regresa del hilo pero mantiene viva la solicitud). No funciona tan bien para los programas de consola: Main regresa al sistema operativo, por lo que su programa se cierra.

Una solución es proporcionar su propio contexto: un "bucle principal" para su programa de consola que sea compatible con la sincronización.

Si tiene una máquina con el CTP asíncrono, puede usar GeneralThreadAffineContextdesde Mis documentos \ Microsoft Visual Studio CTP asíncrono \ Muestras (Prueba de C #) Prueba de unidad \ AsyncTestUtilities . Alternativamente, puede usar AsyncContextdesde mi paquete Nito.AsyncEx NuGet .

Aquí hay un ejemplo usando AsyncContext; GeneralThreadAffineContexttiene un uso casi idéntico:

using Nito.AsyncEx;
class Program
{
    static void Main(string[] args)
    {
        AsyncContext.Run(() => MainAsync(args));
    }

    static async void MainAsync(string[] args)
    {
        Bootstrapper bs = new Bootstrapper();
        var list = await bs.GetList();
    }
}

Alternativamente, puede bloquear el hilo principal de la consola hasta que su trabajo asincrónico se haya completado:

class Program
{
    static void Main(string[] args)
    {
        MainAsync(args).GetAwaiter().GetResult();
    }

    static async Task MainAsync(string[] args)
    {
        Bootstrapper bs = new Bootstrapper();
        var list = await bs.GetList();
    }
}

Tenga en cuenta el uso de GetAwaiter().GetResult(); esto evita la AggregateExceptionenvoltura que ocurre si usa Wait()o Result.

Actualización, 2017-11-30: a partir de Visual Studio 2017 Actualización 3 (15.3), el idioma ahora admite un async Main- siempre que regrese Tasko Task<T>. Entonces ahora puedes hacer esto:

class Program
{
    static async Task Main(string[] args)
    {
        Bootstrapper bs = new Bootstrapper();
        var list = await bs.GetList();
    }
}

La semántica parece ser la misma que el GetAwaiter().GetResult()estilo de bloquear el hilo principal. Sin embargo, todavía no hay especificaciones de lenguaje para C # 7.1, por lo que esto es solo una suposición.

Stephen Cleary
fuente
30
Usted puede usar un simple Waito Result, y no hay nada de malo en eso. Pero tenga en cuenta que hay dos diferencias importantes: 1) todas las asynccontinuaciones se ejecutan en el grupo de subprocesos en lugar del subproceso principal, y 2) las excepciones se envuelven en un AggregateException.
Stephen Cleary
2
Tenía un problema real para resolver esto hasta esto (y su publicación de blog). Este es, con mucho, el método más fácil para resolver este problema, y ​​puede instalar el paquete en la consola nuget con solo un "paquete de instalación Nito.Asyncex" y ya está.
ConstantineK
1
@StephenCleary: Gracias por la rápida respuesta Stephen. No entiendo por qué alguien no se quiere que el depurador se rompa cuando se produce una excepción. Si estoy depurando y encuentro una excepción de referencia nula, parece preferible ir directamente a la línea de código ofensiva. VS funciona así "fuera de la caja" para el código sincrónico, pero no para async / wait.
Greg
66
C # 7.1 tiene un asíncrono principal ahora, podría valer la pena agregarlo a su gran respuesta, @StephenCleary github.com/dotnet/csharplang/blob/master/proposals/csharp-7.1/…
Mafii
3
Si usa la versión C # 7.1 en VS 2017, necesitaba asegurarme de que el proyecto estaba configurado para usar la última versión del lenguaje al agregarlo <LangVersion>latest</LangVersion>al archivo csproj, como se muestra aquí .
Liam
359

Puede resolver esto con esta construcción simple:

class Program
{
    static void Main(string[] args)
    {
        Task.Run(async () =>
        {
            // Do any async anything you need here without worry
        }).GetAwaiter().GetResult();
    }
}

Eso colocará todo lo que haga en el ThreadPool donde lo desee (por lo que otras Tareas que inicie / espere no intenten unirse a un Thread que no deberían), y espere hasta que todo esté listo antes de cerrar la aplicación Consola. No necesita bucles especiales ni libs externos.

Editar: incorpore la solución de Andrew para excepciones no detectadas.

Chris Moschini
fuente
3
Este enfoque es muy obvio, pero tiende a cerrar excepciones, por lo que ahora estoy buscando una mejor manera.
abatishchev
2
@abatishchev Debería usar try / catch en su código, al menos dentro de la Tarea. Ejecute si no de forma más granular, sin dejar que las excepciones floten hasta la Tarea. Evitará el problema de cierre colocando try / catch alrededor de cosas que pueden fallar.
Chris Moschini
54
Si reemplaza Wait()con GetAwaiter().GetResult()evitará la AggregateExceptionenvoltura cuando las cosas se tiran.
Andrew Arnott
77
Así es como async mainse está introduciendo en C # 7.1, a partir de este escrito.
user9993
@ user9993 Según esta propuesta , eso no es exactamente cierto.
Sinjai
90

Puede hacer esto sin necesidad de bibliotecas externas también haciendo lo siguiente:

class Program
{
    static void Main(string[] args)
    {
        Bootstrapper bs = new Bootstrapper();
        var getListTask = bs.GetList(); // returns the Task<List<TvChannel>>

        Task.WaitAll(getListTask); // block while the task completes

        var list = getListTask.Result;
    }
}
Steven Evers
fuente
77
Tenga en cuenta que getListTask.Resulttambién es una llamada de bloqueo, por lo que el código anterior podría escribirse sin él Task.WaitAll(getListTask).
do0g
27
Además, si GetListlanza, tendrá que atrapar AggregateExceptione interrogar sus excepciones para determinar la excepción real lanzada. Sin embargo, puede llamar GetAwaiter()para obtener el TaskAwaiterpara Task, y llamar GetResult()a eso, es decir var list = getListTask.GetAwaiter().GetResult();. Al obtener el resultado de TaskAwaiter(también una llamada de bloqueo), las excepciones lanzadas no se incluirán en un AggregateException.
do0g
1
.GetAwaiter (). GetResult fue la respuesta que necesitaba. Eso funciona perfectamente para lo que estaba intentando hacer. Probablemente usaré esto en otros lugares también.
Deathstalker
78

En C # 7.1 podrá hacer una sincronización asíncrona adecuada . Las firmas apropiadas para el Mainmétodo se han extendido a:

public static Task Main();
public static Task<int> Main();
public static Task Main(string[] args);
public static Task<int> Main(string[] args);

Por ejemplo, podrías estar haciendo:

static async Task Main(string[] args)
{
    Bootstrapper bs = new Bootstrapper();
    var list = await bs.GetList();
}

En tiempo de compilación, el método del punto de entrada asíncrono se traducirá a la llamada GetAwaitor().GetResult().

Detalles: https://blogs.msdn.microsoft.com/mazhou/2017/05/30/c-7-series-part-2-async-main

EDITAR:

Para habilitar las características del lenguaje C # 7.1, debe hacer clic derecho en el proyecto y hacer clic en "Propiedades" y luego ir a la pestaña "Crear". Allí, haga clic en el botón avanzado en la parte inferior:

ingrese la descripción de la imagen aquí

En el menú desplegable de la versión de idioma, seleccione "7.1" (o cualquier valor superior):

ingrese la descripción de la imagen aquí

El valor predeterminado es "última versión principal" que evaluaría (en el momento de escribir este artículo) a C # 7.0, que no es compatible con async main en las aplicaciones de consola.

nawfal
fuente
2
FWIW está disponible en Visual Studio 15.3 y versiones posteriores, que actualmente está disponible como versión beta / versión preliminar desde aquí: visualstudio.com/vs/preview
Mahmoud Al-Qudsi
Espera un minuto ... Estoy ejecutando una instalación completamente actualizada y mi última opción es 7.1 ... ¿cómo obtuviste 7.2 ya en mayo?
La respuesta de mayo fue mía. La edición de octubre fue realizada por otra persona cuando creo que 7.2 (¿vista previa?) Podría haberse lanzado.
nawfal
1
Atención: compruebe que esté en todas las configuraciones, ¡no solo depure cuando haga esto!
user230910
1
@ user230910 gracias. Una de las elecciones más raras del equipo de c #.
nawfal
74

Agregaré una característica importante que todas las otras respuestas han pasado por alto: la cancelación.

Una de las grandes cosas de TPL es el soporte de cancelación, y las aplicaciones de consola tienen un método de cancelación incorporado (CTRL + C). Es muy simple unirlos. Así es como estructuro todas mis aplicaciones de consola asíncronas:

static void Main(string[] args)
{
    CancellationTokenSource cts = new CancellationTokenSource();

    System.Console.CancelKeyPress += (s, e) =>
    {
        e.Cancel = true;
        cts.Cancel();
    };

    MainAsync(args, cts.Token).Wait();
}

static async Task MainAsync(string[] args, CancellationToken token)
{
    ...
}
Cory Nelson
fuente
¿Debería pasarse también el token de cancelación Wait()?
Siewers
55
No, porque desea que el código asíncrono pueda manejar con gracia la cancelación. Si lo pasa al Wait(), no esperará a que termine el código asincrónico; dejará de esperar y finalizará el proceso de inmediato.
Cory Nelson
¿Estás seguro de eso? Lo acabo de probar y parece que la solicitud de cancelación se está procesando en el nivel más profundo, incluso cuando el Wait()método pasa el mismo token. Lo que intento decir es que no parece hacer ninguna diferencia.
Siewers
44
Estoy seguro. Desea cancelar la operación en sí, no la espera de que termine la operación. A menos que no le importe el acabado del código de limpieza o el resultado del mismo.
Cory Nelson
1
Sí, creo que lo entiendo, simplemente no parecía hacer ninguna diferencia en mi código. Otra cosa que me desvió del curso fue una indirecta cortés de ReSharper sobre el método de espera que respalda la cancelación;) Es posible que desee incluir una prueba de captura en el ejemplo, ya que arrojará una OperationCancelledException, que no pude entender al principio
Siewers
22

C # 7.1 (usando vs 2017 actualización 3) presenta async main

Puedes escribir:

   static async Task Main(string[] args)
  {
    await ...
  }

Para más detalles C # 7 Series, Part 2: Async Main

Actualizar:

Puede obtener un error de compilación:

El programa no contiene un método estático 'Principal' adecuado para un punto de entrada

Este error se debe a que vs2017.3 está configurado de forma predeterminada como c # 7.0 no c # 7.1.

Debe modificar explícitamente la configuración de su proyecto para configurar las características de c # 7.1.

Puede configurar c # 7.1 por dos métodos:

Método 1: Usando la ventana de configuración del proyecto:

  • Abra la configuración de su proyecto
  • Seleccione la pestaña Construir
  • Haga clic en el botón Avanzado
  • Seleccione la versión que desee Como se muestra en la siguiente figura:

ingrese la descripción de la imagen aquí

Método 2: Modificar manualmente PropertyGroup de .csproj

Añadir esta propiedad:

    <LangVersion>7.1</LangVersion>

ejemplo:

    <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
        <PlatformTarget>AnyCPU</PlatformTarget>
        <DebugSymbols>true</DebugSymbols>
        <DebugType>full</DebugType>
        <Optimize>false</Optimize>
        <OutputPath>bin\Debug\</OutputPath>
        <DefineConstants>DEBUG;TRACE</DefineConstants>
        <ErrorReport>prompt</ErrorReport>
        <WarningLevel>4</WarningLevel>
        <Prefer32Bit>false</Prefer32Bit>
        <LangVersion>7.1</LangVersion>
    </PropertyGroup>    
M.Hassan
fuente
20

Si está utilizando C # 7.1 o posterior, vaya con la respuesta de nawfal y simplemente cambie el tipo de retorno de su método Main a Tasko Task<int>. Si no eres:

  • Tener async Task MainAsync como dijo Johan .
  • Llame a esto .GetAwaiter().GetResult()para atrapar la excepción subyacente como dijo do0g .
  • Cancelación de soporte como dijo Cory .
  • Un segundo CTRL+Cdebe terminar el proceso de inmediato. (¡Gracias, binki !)
  • Handle OperationCancelledException: devuelve un código de error apropiado.

El código final se ve así:

private static int Main(string[] args)
{
    var cts = new CancellationTokenSource();
    Console.CancelKeyPress += (s, e) =>
    {
        e.Cancel = !cts.IsCancellationRequested;
        cts.Cancel();
    };

    try
    {
        return MainAsync(args, cts.Token).GetAwaiter().GetResult();
    }
    catch (OperationCanceledException)
    {
        return 1223; // Cancelled.
    }
}

private static async Task<int> MainAsync(string[] args, CancellationToken cancellationToken)
{
    // Your code...

    return await Task.FromResult(0); // Success.
}
Şafak Gür
fuente
1
Muchos programas agradables cancelarán CancelKeyPress solo la primera vez, de modo que si presiona ^ C una vez que obtiene un apagado correcto pero si está impaciente, un segundo ^ C termina sin gracia. Con esta solución, necesitaría eliminar manualmente el programa si no cumple con CancellationToken porque e.Cancel = truees incondicional.
binki
19

Todavía no he necesitado tanto, pero cuando utilicé la aplicación de consola para las pruebas rápidas y requirí asíncrono, lo resolví así:

class Program
{
    static void Main(string[] args)
    {
        MainAsync(args).Wait();
    }

    static async Task MainAsync(string[] args)
    {
        // Code here
    }
}
Johan Falk
fuente
Este ejemplo funcionará mal en caso de que tenga que programar la tarea en el contexto actual y luego esperarla (por ejemplo, puede olvidar agregar ConfigureAwait (falso), por lo que el método de retorno se programará en el hilo principal, que está en la función Esperar ) Como el hilo actual está en estado de espera, recibirá un punto muerto.
Manushin Igor
66
No es cierto, @ManushinIgor. Al menos en este ejemplo trivial, no hay SynchronizationContextasociado con el hilo principal. Por lo tanto, no se bloqueará porque incluso sin él ConfigureAwait(false), todas las continuaciones se ejecutarán en el conjunto de hilos.
Andrew Arnott
4

En Principal, intente cambiar la llamada a GetList por:

Task.Run(() => bs.GetList());
mysticdotnet
fuente
4

Cuando se introdujo el C # 5 CTP, ciertamente podría marcar Main conasync ... aunque generalmente no era una buena idea hacerlo. Creo que esto fue cambiado por el lanzamiento de VS 2013 para convertirse en un error.

A menos que haya iniciado otros hilos de primer plano , su programa se cerrará cuandoMain complete, incluso si ha comenzado un trabajo de fondo.

¿Qué es lo que realmente intentas hacer? Tenga en cuenta que su GetList()método realmente no necesita ser asíncrono en este momento: está agregando una capa adicional sin ninguna razón real. Es lógicamente equivalente a (pero más complicado que):

public Task<List<TvChannel>> GetList()
{
    return new GetPrograms().DownloadTvChannels();
}
Jon Skeet
fuente
2
Jon, quiero obtener los elementos de la lista de forma asincrónica, entonces, ¿por qué la sincronización no es adecuada en ese método GetList? ¿Es porque necesito recopilar los elementos de la lista asincrónica y no la lista misma? Cuando trato de marcar el método Main con asíncrono obtengo un "no contiene un método Main estático ..."
danielovich
@danielovich: ¿Qué DownloadTvChannels()devuelve? Presumiblemente devuelve un Task<List<TvChannel>>¿no? Si no, es poco probable que puedas esperarlo. (Posible, dado el patrón de camarero, pero improbable). En cuanto al Mainmétodo, aún debe ser estático ... ¿ quizás reemplazó el staticmodificador con el asyncmodificador?
Jon Skeet
sí, devuelve una Tarea <..> como dijiste. No importa cómo intente poner asíncrono en la firma del método Main, arroja un error. Estoy sentado en VS11 bits de vista previa!
danielovich
@danielovich: ¿Incluso con un tipo de retorno nulo? Solo public static async void Main() {}? Pero si DownloadTvChannels()ya devuelve un Task<List<TvChannel>>, presumiblemente ya es asíncrono, por lo que no necesita agregar otra capa. Vale la pena entender esto con cuidado.
Jon Skeet
1
@nawfal: Mirando hacia atrás, creo que cambió antes de que se lanzara VS2013. No estoy seguro si C # 7 va a cambiar eso ...
Jon Skeet
4

La versión más nueva de C # - C # 7.1 permite crear una aplicación de consola asíncrona. Para habilitar C # 7.1 en el proyecto, debe actualizar su VS al menos a 15.3 y cambiar la versión de C # a C# 7.1o C# latest minor version. Para hacer esto, vaya a Propiedades del proyecto -> Compilación -> Avanzado -> Versión del idioma.

Después de esto, el siguiente código funcionará:

internal class Program
{
    public static async Task Main(string[] args)
    {
         (...)
    }
Kedrzu
fuente
3

En MSDN, la documentación del Método Task.Run (Acción) proporciona este ejemplo que muestra cómo ejecutar un método de forma asíncrona desde main:

using System;
using System.Threading;
using System.Threading.Tasks;

public class Example
{
    public static void Main()
    {
        ShowThreadInfo("Application");

        var t = Task.Run(() => ShowThreadInfo("Task") );
        t.Wait();
    }

    static void ShowThreadInfo(String s)
    {
        Console.WriteLine("{0} Thread ID: {1}",
                          s, Thread.CurrentThread.ManagedThreadId);
    }
}
// The example displays the following output:
//       Application thread ID: 1
//       Task thread ID: 3

Tenga en cuenta esta declaración que sigue el ejemplo:

Los ejemplos muestran que la tarea asincrónica se ejecuta en un subproceso diferente que el subproceso principal de la aplicación.

Entonces, si en cambio desea que la tarea se ejecute en el hilo principal de la aplicación, vea la respuesta de @StephenCleary .

Y con respecto al hilo en el que se ejecuta la tarea, también tenga en cuenta Stephen's comentario sobre su respuesta:

Usted puede usar un simple Waito Result, y no hay nada de malo en eso. Pero tenga en cuenta que hay dos diferencias importantes: 1) todasasync continuaciones se ejecutan en el grupo de subprocesos en lugar del subproceso principal, y 2) las excepciones se envuelven en un AggregateException.

(Consulte Manejo de excepciones (Biblioteca paralela de tareas) para obtener información sobre cómo incorporar el manejo de excepciones para tratar con un AggregateException.)


Finalmente, en MSDN de la documentación para Task.Delay Method (TimeSpan) , este ejemplo muestra cómo ejecutar una tarea asincrónica que devuelve un valor:

using System;
using System.Threading.Tasks;

public class Example
{
    public static void Main()
    {
        var t = Task.Run(async delegate
                {
                    await Task.Delay(TimeSpan.FromSeconds(1.5));
                    return 42;
                });
        t.Wait();
        Console.WriteLine("Task t Status: {0}, Result: {1}",
                          t.Status, t.Result);
    }
}
// The example displays the following output:
//        Task t Status: RanToCompletion, Result: 42

Tenga en cuenta que en lugar de pasar un delegatea Task.Run, puede pasar una función lambda como esta:

var t = Task.Run(async () =>
        {
            await Task.Delay(TimeSpan.FromSeconds(1.5));
            return 42;
        });
DavidRR
fuente
1

Para evitar la congelación cuando llama a una función en algún lugar de la pila de llamadas que intenta volver a unirse al hilo actual (que está atascado en una Espera), debe hacer lo siguiente:

class Program
{
    static void Main(string[] args)
    {
        Bootstrapper bs = new Bootstrapper();
        List<TvChannel> list = Task.Run((Func<Task<List<TvChannel>>>)bs.GetList).Result;
    }
}

(el reparto solo es necesario para resolver la ambigüedad)

Nathan Phillips
fuente
Gracias; Task.Run no causa el punto muerto de GetList (). Espera, esta respuesta debería tener más votos a favor ...
Stefano d'Antonio
1

En mi caso, tenía una lista de trabajos que quería ejecutar de forma asíncrona desde mi método principal, he estado usando esto en producción durante bastante tiempo y funciona bien.

static void Main(string[] args)
{
    Task.Run(async () => { await Task.WhenAll(jobslist.Select(nl => RunMulti(nl))); }).GetAwaiter().GetResult();
}
private static async Task RunMulti(List<string> joblist)
{
    await ...
}
usuario_v
fuente