Não é possível especificar o modificador 'async' no método 'Main' de um aplicativo de console


445

Eu sou novo na programação assíncrona com o asyncmodificador. Estou tentando descobrir como garantir que meu Mainmétodo de aplicativo de console seja executado de forma assíncrona.

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();
    }
}

Eu sei que isso não está sendo executado de forma assíncrona a partir do "topo". Como não é possível especificar o asyncmodificador no Mainmétodo, como posso executar o código de mainforma assíncrona?


23
Esse não é mais o caso do C # 7.1. Métodos principais podem ser assíncrono
Vasily Sliounaiev

2
Aqui está o anúncio de postagem do blog em C # 7.1 . Veja a seção intitulada Async Main .
Styfle 3/11

Respostas:


382

Como você descobriu, no VS11, o compilador não permitirá um async Mainmétodo. Isso foi permitido (mas nunca recomendado) no VS2010 com o CTP assíncrono.

Tenho postagens recentes no blog sobre os programas de console assíncrono / aguardado e assíncrono em particular. Aqui estão algumas informações básicas da publicação de introdução:

Se "aguardar" perceber que o aguardável não foi concluído, ele age de forma assíncrona. Ele diz ao aguardável para executar o restante do método quando ele é concluído e retorna do método assíncrono. Await também capturará o contexto atual quando passar o restante do método para o aguardável.

Posteriormente, quando o aguardável for concluído, ele executará o restante do método assíncrono (dentro do contexto capturado).

Veja por que esse problema ocorre nos programas de console com um async Main:

Lembre-se do nosso post de introdução que um método assíncrono retornará ao chamador antes de ser concluído. Isso funciona perfeitamente em aplicativos de interface do usuário (o método apenas retorna ao loop de eventos da interface do usuário) e aplicativos ASP.NET (o método retorna do thread, mas mantém a solicitação ativa). Não funciona tão bem para os programas do console: Main retorna ao sistema operacional - para que seu programa saia.

Uma solução é fornecer seu próprio contexto - um "loop principal" para o seu programa de console compatível com async.

Se você tiver uma máquina com o CTP assíncrono, poderá usar GeneralThreadAffineContextem Meus Documentos \ Microsoft Visual Studio CTP \ Amostras assíncronas \ Amostras (teste de C #) Testing \ AsyncTestUtilities . Alternativamente, você pode usar AsyncContexta partir de meu pacote Nito.AsyncEx NuGet .

Aqui está um exemplo usando AsyncContext; GeneralThreadAffineContexttem uso quase 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();
    }
}

Como alternativa, você pode apenas bloquear o segmento principal do console até que seu trabalho assíncrono seja concluído:

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();
    }
}

Observe o uso de GetAwaiter().GetResult(); isso evita o AggregateExceptionacondicionamento que acontece se você usar Wait()ou Result.

Atualização, 30/11/2017: A partir da Atualização 3 do Visual Studio 2017 (15.3), o idioma agora suporta um async Main- desde que retorne Taskou Task<T>. Agora você pode fazer isso:

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

A semântica parece ser a mesma do GetAwaiter().GetResult()estilo de bloqueio do encadeamento principal. No entanto, ainda não há especificação de idioma para o C # 7.1, portanto, isso é apenas uma suposição.


30
Você pode usar um simples Waitou Result, e não há nada de errado nisso. Mas esteja ciente de que existem duas diferenças importantes: 1) todas as asynccontinuações são executadas no conjunto de encadeamentos, e não no encadeamento principal, e 2) todas as exceções são agrupadas em um AggregateException.
Stephen Cleary

2
Estava tendo um problema real para descobrir isso até isso (e sua postagem no blog). Esse é de longe o método mais fácil de resolver esse problema, e você pode instalar o pacote no console do nuget com apenas um "pacote de instalação Nito.Asyncex" e pronto.
ConstantineK

1
@ StephenCleary: Obrigado pela resposta rápida Stephen. Não entendo por que alguém não gostaria que o depurador fosse interrompido quando uma exceção fosse lançada. Se estou depurando e deparei com uma exceção de referência nula, é preferível ir diretamente para a linha de código incorreta. O VS funciona assim "fora da caixa" para código síncrono, mas não para assíncrono / espera.
Greg

6
O C # 7.1 tem um main assíncrono agora, pode valer a pena adicionar à sua ótima resposta, @StephenCleary github.com/dotnet/csharplang/blob/master/proposals/csharp-7.1/…
Mafii

3
Se você estiver usando a versão C # 7.1 no VS 2017, precisava garantir que o projeto fosse configurado para usar a versão mais recente do idioma, adicionando <LangVersion>latest</LangVersion>ao arquivo csproj, como mostrado aqui .
Liam

359

Você pode resolver isso com essa construção simples:

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

Isso colocará tudo o que você faz no ThreadPool onde você o deseja (para que outras tarefas iniciadas / aguardadas não tentem voltar a um segmento que não deveriam) e espere até que tudo esteja pronto antes de fechar o aplicativo do console. Não há necessidade de loops especiais ou bibliotecas externas.

Edit: Incorpore a solução de Andrew para Exceções não capturadas.


3
Essa abordagem é muito óbvia, mas tende a quebrar exceções, então estou procurando agora uma maneira melhor.
precisa saber é

2
@abatishchev Você deve usar try / catch no seu código, pelo menos dentro da Task.Run se não for mais granular, não deixando exceções flutuarem até a Task. Você evitará o problema de finalização colocando try / catch em torno de coisas que podem falhar.
Chris Moschini 23/03

54
Se você substituir Wait()com GetAwaiter().GetResult()você vai evitar o AggregateExceptioninvólucro quando as coisas jogar.
Andrew Arnott

7
É assim que async mainestá sendo introduzido no C # 7.1, até o momento em que este artigo foi escrito.
user9993

@ user9993 De acordo com esta proposta , isso não é exatamente verdade.
Sinjai 17/01/19

90

Você pode fazer isso sem precisar de bibliotecas externas também fazendo o seguinte:

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;
    }
}

7
Lembre-se de que getListTask.Resulttambém é uma chamada de bloqueio e, portanto, o código acima pode ser escrito sem Task.WaitAll(getListTask).
do0g

27
Além disso, se GetListjogar, você terá que pegar um AggregateExceptione interrogar suas exceções para determinar a exceção real lançada. Você pode, no entanto, ligar GetAwaiter()para obter o valor TaskAwaiterpara Task, e chamar GetResult()isso, ie var list = getListTask.GetAwaiter().GetResult();. Ao obter o resultado da TaskAwaiter(também uma chamada de bloqueio), as exceções lançadas não serão agrupadas em um AggregateException.
do0g

1
.GetAwaiter (). GetResult foi a resposta que eu precisava. Isso funciona perfeitamente para o que eu estava tentando fazer. Provavelmente vou usar isso em outros lugares também.
Deathstalker

78

No C # 7.1, você poderá fazer um Main assíncrono adequado . As assinaturas apropriadas para o Mainmétodo foram estendidas para:

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

Por exemplo, você poderia estar fazendo:

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

Em tempo de compilação, o método do ponto de entrada assíncrono será convertido para chamar GetAwaitor().GetResult().

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

EDITAR:

Para habilitar os recursos de linguagem C # 7.1, clique com o botão direito do mouse no projeto e clique em "Propriedades" e, em seguida, vá para a guia "Compilar". Lá, clique no botão avançado na parte inferior:

insira a descrição da imagem aqui

No menu suspenso da versão do idioma, selecione "7.1" (ou qualquer valor mais alto):

insira a descrição da imagem aqui

O padrão é a "versão principal mais recente", que seria avaliada (no momento da redação deste documento) para o C # 7.0, que não suporta o principal assíncrono nos aplicativos de console.


2
FWIW este está disponível no Visual Studio 15.3 e acima, que está atualmente disponível como uma versão beta / preview a partir daqui: visualstudio.com/vs/preview
Mahmoud Al-Qudsi

Espere um minuto ... Estou executando uma instalação totalmente atualizada e minha última opção é 7.1 ... como você conseguiu o 7.2 já em maio?

A resposta de maio foi minha. A edição de outubro foi feita por outra pessoa quando acho que o 7.2 (pré-visualização?) Pode ter sido lançado.
Nawfal #

1
Atenção - verifique se está em todas as configurações, não apenas depure quando você fizer isso!
user230910

1
@ user230910 obrigado. Uma das escolhas mais estranhas da equipe de c #.
Nawfal

74

Vou adicionar um recurso importante que todas as outras respostas ignoraram: cancelamento.

Uma das grandes vantagens do TPL é o suporte ao cancelamento, e os aplicativos de console têm um método de cancelamento incorporado (CTRL + C). É muito simples uni-los. É assim que eu estruturo todos os meus aplicativos de console assíncrono:

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)
{
    ...
}

O token de cancelamento também deve ser passado para o Wait()?
Siewers 14/10

5
Não, porque você deseja que o código assíncrono possa lidar com o cancelamento normalmente. Se você passá- Wait()lo para o arquivo, ele não esperará o código assíncrono terminar - ele parará de esperar e encerrará o processo imediatamente.
Cory Nelson

Você tem certeza sobre isso? Acabei de experimentar e parece que a solicitação de cancelamento está sendo processada no nível mais profundo, mesmo quando o Wait()método recebe o mesmo token. O que estou tentando dizer é que isso não parece fazer nenhuma diferença.
Siewers

4
Tenho certeza. Você deseja cancelar a operação em si, não a espera pela conclusão da operação. A menos que você não se importe com o acabamento do código de limpeza ou com o resultado dele.
Cory Nelson

1
Sim, acho que entendi, simplesmente não pareceu fazer nenhuma diferença no meu código. Outra coisa que me tirou o rumo foi uma sugestão educada do ReSharper sobre o método de espera que suporta o cancelamento;) Você pode incluir uma tentativa de captura no exemplo, pois ele lançará uma OperationCancelledException, que eu não consegui descobrir a princípio
Siewers

22

O C # 7.1 (usando vs 2017 atualização 3) apresenta o principal assíncrono

Você pode escrever:

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

Para obter mais detalhes Série C # 7, Parte 2: Principal assíncrono

Atualizar:

Você pode receber um erro de compilação:

O programa não contém um método 'Principal' estático adequado para um ponto de entrada

Esse erro ocorre porque o vs2017.3 está configurado por padrão como c # 7.0 e não c # 7.1.

Você deve modificar explicitamente a configuração do seu projeto para definir os recursos do c # 7.1.

Você pode definir o c # 7.1 por dois métodos:

Método 1: usando a janela de configurações do projeto:

  • Abra as configurações do seu projeto
  • Selecione a guia Compilar
  • Clique no botão Avançado
  • Selecione a versão desejada Como mostrado na figura a seguir:

insira a descrição da imagem aqui

Método2: Modificar PropertyGroup de .csproj manualmente

Adicione esta propriedade:

    <LangVersion>7.1</LangVersion>

exemplo:

    <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>    

20

Se você estiver usando o C # 7.1 ou posterior, siga a resposta do nawfal e altere o tipo de retorno do seu método Main para Taskou Task<int>. Se você não está:

  • Tenha async Task MainAsync como Johan disse .
  • Chame seu .GetAwaiter().GetResult()para capturar a exceção subjacente, como do0g disse .
  • Suporte cancelamento como Cory disse .
  • Um segundo CTRL+Cdeve encerrar o processo imediatamente. (Obrigado binki !)
  • Alça OperationCancelledException- retorna um código de erro apropriado.

O código final se parece com:

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.
}

1
Muitos programas legais cancelam o CancelKeyPress apenas na primeira vez, de modo que, se você pressionar ^ C depois de obter um desligamento normal, mas se estiver impaciente, um segundo ^ C será encerrado sem graça. Com esta solução, você precisará matar manualmente o programa se ele não atender ao CancellationToken porque e.Cancel = trueé incondicional.
binki

19

Ainda não precisei muito disso, mas quando usei o aplicativo de console para testes rápidos e assíncrono necessário, resolvi-o assim:

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

    static async Task MainAsync(string[] args)
    {
        // Code here
    }
}

Este exemplo funcionará errado caso você precise agendar uma tarefa para o contexto atual e aguardá-la (por exemplo, você pode esquecer de adicionar ConfigureAwait (false), para que o método de retorno seja agendado no thread principal, que está na função Wait ) Como o encadeamento atual está no estado de espera, você receberá um impasse.
Manushin Igor 23/09/2015

6
Não é verdade, @ManushinIgor. Pelo menos neste exemplo trivial, não há SynchronizationContextassociado ao encadeamento principal. Portanto, não haverá impasse porque, mesmo sem ConfigureAwait(false), todas as continuações serão executadas no pool de threads.
Andrew Arnott


4

Em Main, tente alterar a chamada para GetList para:

Task.Run(() => bs.GetList());

4

Quando o CTP C # 5 foi introduzido, você certamente pode marcar Main como async... embora geralmente não seja uma boa ideia fazê-lo. Acredito que isso foi alterado pelo lançamento do VS 2013 para se tornar um erro.

A menos que você tenha iniciado outros threads em primeiro plano , seu programa será encerrado quando for Mainconcluído, mesmo que tenha sido iniciado algum trabalho em segundo plano.

O que você realmente está tentando fazer? Observe que seu GetList()método realmente não precisa ser assíncrono no momento - está adicionando uma camada extra sem motivo real. É logicamente equivalente a (mas mais complicado que):

public Task<List<TvChannel>> GetList()
{
    return new GetPrograms().DownloadTvChannels();
}

2
Jon, quero obter os itens da lista de forma assíncrona, então por que o assíncrono não é apropriado nesse método GetList? É porque eu preciso coletar os itens da lista assíncrona 'e não a própria lista? Quando eu tento marcar o método principal com assíncrona eu recebo um "não contém um método estático principal ..."
danielovich

@danielovich: O que DownloadTvChannels()retorna? Presumivelmente, ele retorna um Task<List<TvChannel>>, não é? Caso contrário, é improvável que você possa aguardar. (Possível, dado o padrão do garçom, mas improvável.) Quanto ao Mainmétodo - ele ainda precisa ser estático ... você substituiu o staticmodificador pelo asyncmodificador, talvez?
9116 Jon Skeet

sim, ele retorna uma tarefa <..> como você disse. Não importa como eu tente colocar assíncrono na assinatura do método Main, ele gera um erro. Estou sentado nos bits de visualização do VS11!
Danielovich

@danielovich: Mesmo com um tipo de retorno nulo? Apenas public static async void Main() {}? Mas se DownloadTvChannels()já retorna umTask<List<TvChannel>> , presumivelmente já é assíncrono - para que você não precise adicionar outra camada. Vale a pena entender isso com cuidado.
Jon Skeet

1
@nawfal: Olhando para trás, acho que mudou antes do lançamento do VS2013. Não tenho certeza se o C # 7 vai mudar isso ...
Jon Skeet

4

A versão mais recente do C # - C # 7.1 permite criar um aplicativo de console assíncrono. Para habilitar o C # 7.1 no projeto, você deve atualizar seu VS para pelo menos 15.3 e alterar a versão do C # para C# 7.1ou C# latest minor version. Para fazer isso, vá para Propriedades do projeto -> Compilar -> Avançado -> Versão do idioma.

Depois disso, o seguinte código funcionará:

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

3

No MSDN, a documentação do método Task.Run (Action) fornece este exemplo que mostra como executar um método de forma assíncrona em 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

Observe esta declaração que segue o exemplo:

Os exemplos mostram que a tarefa assíncrona é executada em um segmento diferente do segmento principal do aplicativo.

Portanto, se você deseja que a tarefa seja executada no encadeamento principal do aplicativo, consulte a resposta em @StephenCleary .

E sobre o segmento em que a tarefa é executada, observe também o comentário de Stephen em sua resposta:

Você pode usar um simples Waitou Result, e não há nada de errado nisso. Mas esteja ciente de que existem duas diferenças importantes: 1) todas as asynccontinuações são executadas no conjunto de encadeamentos, e não no encadeamento principal, e 2) todas as exceções são agrupadas em um AggregateException.

(Consulte Tratamento de exceções (Task Parallel Library) para saber como incorporar o tratamento de exceções para lidar com umAggregateException .)


Finalmente, no MSDN da documentação do método Task.Delay (TimeSpan) , este exemplo mostra como executar uma tarefa assíncrona que retorna um 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

Observe que, em vez de passar delegatepara Task.Run, você pode passar uma função lambda como esta:

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

1

Para evitar o congelamento quando você chama uma função em algum lugar da pilha de chamadas que tenta ingressar novamente no segmento atual (que está preso em uma espera), faça o seguinte:

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

(o elenco é necessário apenas para resolver ambiguidade)


Obrigado; Não Task.Run não causar o bloqueio de GetList () Espere, esta resposta shold ter mais upvotes ....
Stefano d'Antonio

1

No meu caso, eu tinha uma lista de tarefas que eu queria executar em assíncrona a partir do meu método principal, tenho usado isso na produção há algum tempo e funciona bem.

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 ...
}
Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.