Impossibile specificare il modificatore "asincrono" sul metodo "Principale" di un'app console


445

Sono nuovo alla programmazione asincrona con il asyncmodificatore. Sto cercando di capire come assicurarmi che il mio Mainmetodo di un'applicazione console funzioni effettivamente in modo asincrono.

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

So che questo non funziona in modo asincrono dalla "cima". Poiché non è possibile specificare il asyncmodificatore sul Mainmetodo, come posso eseguire il codice in mainmodo asincrono?


23
Questo non è più il caso in C # 7.1. I metodi principali possono essere asincroni
Vasily Sliounaiev,

2
Ecco l' annuncio del post sul blog C # 7.1 . Vedi la sezione intitolata Async Main .
Styfle

Risposte:


382

Come hai scoperto, in VS11 il compilatore non consentirà un async Mainmetodo. Ciò è stato consentito (ma mai consigliato) in VS2010 con Async CTP.

Ho post recenti sul blog in particolare su programmi asincroni / waitit e console asincrone . Ecco alcune informazioni di base dal post introduttivo:

Se "wait" vede che l'atteso non è stato completato, agisce in modo asincrono. Indica l'attesa di eseguire il resto del metodo al termine, quindi ritorna dal metodo asincrono. Attendere inoltre catturerà il contesto attuale quando passa il resto del metodo all'atteso.

Più tardi, quando l'atteso sarà completato, eseguirà il resto del metodo asincrono (nel contesto acquisito).

Ecco perché questo è un problema nei programmi della console con un async Main:

Ricorda dal nostro post introduttivo che un metodo asincrono tornerà al suo chiamante prima che sia completo. Funziona perfettamente nelle applicazioni dell'interfaccia utente (il metodo ritorna solo al ciclo degli eventi dell'interfaccia utente) e nelle applicazioni ASP.NET (il metodo restituisce il thread ma mantiene attiva la richiesta). Non funziona così bene per i programmi della console: Main ritorna al sistema operativo, quindi il programma viene chiuso.

Una soluzione consiste nel fornire il proprio contesto: un "ciclo principale" per il programma della console compatibile asincrono.

Se si dispone di un computer con CTP asincrono, è possibile utilizzare GeneralThreadAffineContextda Documenti \ CTP asincrono Microsoft Visual Studio \ Samples (test C #) Test unit \ AsyncTestUtilities . In alternativa, è possibile utilizzare AsyncContextdal mio pacchetto NuGet Nito.AsyncEx .

Ecco un esempio usando AsyncContext; GeneralThreadAffineContextha un utilizzo quasi identico:

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

In alternativa, puoi semplicemente bloccare il thread della console principale fino al completamento del lavoro asincrono:

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

Si noti l'uso di GetAwaiter().GetResult(); questo evita l' AggregateExceptionavvolgimento che si verifica se si utilizza Wait()o Result.

Aggiornamento, 30-11-2017: a partire da Visual Studio 2017 Aggiornamento 3 (15.3), la lingua ora supporta un async Main- fintanto che ritorna Tasko Task<T>. Quindi ora puoi farlo:

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

La semantica sembra essere la stessa dello GetAwaiter().GetResult()stile di blocco del thread principale. Tuttavia, non ci sono ancora specifiche linguistiche per C # 7.1, quindi questo è solo un presupposto.


30
È possibile utilizzare un semplice Waito Result, e non c'è niente di sbagliato in questo. Tuttavia, tieni presente che esistono due differenze importanti: 1) tutte le asynccontinuazioni vengono eseguite sul pool di thread anziché sul thread principale e 2) eventuali eccezioni sono racchiuse in un AggregateException.
Stephen Cleary,

2
Stavo avendo un vero problema per capirlo fino a questo (e il tuo post sul blog). Questo è di gran lunga il metodo più semplice per risolvere questo problema e puoi installare il pacchetto nella console nuget con un "pacchetto di installazione Nito.Asyncex" e il gioco è fatto.
ConstantineK,

1
@StephenCleary: Grazie per la rapida risposta Stephen. Non capisco perché qualcuno non vorrebbe che il debugger si interrompesse quando viene generata un'eccezione. Se sto eseguendo il debug e mi imbatto in un'eccezione di riferimento null, è preferibile passare direttamente alla riga di codice offensiva. VS funziona così "out of the box" per il codice sincrono, ma non per asincrono / wait.
Greg,

6
C # 7.1 ha un asincrono ora, potrebbe valere la pena aggiungere alla tua grande risposta, @StephenCleary github.com/dotnet/csharplang/blob/master/proposals/csharp-7.1/…
Mafii

3
Se stai usando la versione C # 7.1 in VS 2017, dovevo assicurarmi che il progetto fosse configurato per usare l'ultima versione del linguaggio aggiungendo <LangVersion>latest</LangVersion>nel file csproj, come mostrato qui .
Liam,

359

Puoi risolverlo con questo semplice costrutto:

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

Ciò metterà tutto ciò che fai sul ThreadPool dove lo vorresti (quindi altre attività che avvii / attendi non tentano di ricongiungersi a un thread che non dovrebbero) e attendi il completamento di tutto prima di chiudere l'app Console. Non sono necessari loop speciali o librerie esterne.

Modifica: incorporare la soluzione di Andrew per le eccezioni non rilevate.


3
Questo approccio è molto ovvio ma tende a racimolare le eccezioni, quindi sto cercando un modo migliore.
abatishchev,

2
@abatishchev Dovresti usare try / catch nel tuo codice, almeno all'interno dell'attività. Esegui se non in modo più granulare, non lasciare che le eccezioni si spostino sull'attività. Eviterai il problema del riepilogo mettendo in prova cose che possono fallire.
Chris Moschini,

54
Se lo sostituisci Wait()con GetAwaiter().GetResult()eviterai l' AggregateExceptioninvolucro quando le cose si gettano.
Andrew Arnott,

7
Ecco come async mainviene introdotto in C # 7.1, al momento della stesura di questo documento.
user9993

@ user9993 Secondo questa proposta , non è esattamente vero.
Sinjai,

90

Puoi farlo senza la necessità di librerie esterne anche procedendo come segue:

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
Tieni presente che getListTask.Resultè anche una chiamata bloccante e quindi il codice sopra potrebbe essere scritto senza Task.WaitAll(getListTask).
Do0g

27
Inoltre, se i GetListtiri dovrai catturare un AggregateExceptione interrogare le sue eccezioni per determinare l'eccezione effettiva generata. È possibile, tuttavia, chiamare GetAwaiter()per ottenere il TaskAwaiterper Taske chiamare GetResult(), ad es var list = getListTask.GetAwaiter().GetResult();. Quando si ottiene il risultato dalla TaskAwaiter(anche una chiamata di blocco) eventuali eccezioni generate non verranno racchiuse in un AggregateException.
Do0g

1
.GetAwaiter (). GetResult era la risposta di cui avevo bisogno. Funziona perfettamente per quello che stavo tentando di fare. Probabilmente lo userò anche in altri posti.
Deathstalker,

78

In C # 7.1 sarai in grado di eseguire un Main asincrono adeguato . Le firme appropriate per il Mainmetodo sono state estese a:

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

Ad esempio potresti fare:

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

Al momento della compilazione, il metodo del punto di accesso asincrono verrà tradotto in chiamata GetAwaitor().GetResult().

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

MODIFICARE:

Per abilitare le funzionalità del linguaggio C # 7.1, è necessario fare clic con il pulsante destro del mouse sul progetto e fare clic su "Proprietà", quindi andare alla scheda "Crea". Lì, fai clic sul pulsante avanzato in basso:

inserisci qui la descrizione dell'immagine

Dal menu a discesa della versione della lingua, selezionare "7.1" (o qualsiasi valore superiore):

inserisci qui la descrizione dell'immagine

L'impostazione predefinita è "ultima versione principale" che valuterà (al momento della stesura di questo documento) in C # 7.0, che non supporta il principale asincrono nelle app della console.


2
FWIW questo è disponibile in Visual Studio 15.3 e versioni successive, che è attualmente disponibile come versione beta / anteprima da qui: visualstudio.com/vs/preview
Mahmoud Al-Qudsi,

Aspetta un minuto ... Sto eseguendo un'installazione completamente aggiornata e la mia ultima opzione è 7.1 ... come hai ottenuto 7.2 già a maggio?

La risposta di maggio era mia. La modifica di ottobre è stata eseguita da qualcun altro quando penso che 7.2 (anteprima?) Potrebbe essere stato rilasciato.
nawfal,

1
Heads up - controlla che sia su tutte le configurazioni, non solo il debug quando lo fai!
user230910

1
@utente user230910 grazie. Una delle scelte più strane del team c #.
nawfal,

74

Aggiungerò una caratteristica importante che tutte le altre risposte hanno trascurato: la cancellazione.

Una delle caratteristiche principali di TPL è il supporto per la cancellazione e le app per console hanno un metodo di cancellazione integrato (CTRL + C). È molto semplice collegarli insieme. Ecco come strutturo tutte le mie app della console asincrona:

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

Il token di annullamento deve essere passato Wait()anche a?
Siewers,

5
No, perché vuoi che il codice asincrono sia in grado di gestire con grazia la cancellazione. Se lo passi a Wait(), non aspetterà che il codice asincrono finisca - smetterà di aspettare e terminerà immediatamente il processo.
Cory Nelson,

Sei sicuro di questo? L'ho appena provato e sembra che la richiesta di cancellazione venga elaborata al livello più profondo, anche quando il Wait()metodo viene passato lo stesso token. Quello che sto cercando di dire è che non sembra fare alcuna differenza.
Siewers

4
Sono sicuro. Si desidera annullare l'operazione stessa, non l'attesa per il completamento dell'operazione. A meno che non ti interessi la pulizia del codice di finitura o il risultato di esso.
Cory Nelson,

1
Sì, penso di averlo capito, non sembrava fare alcuna differenza nel mio codice. Un'altra cosa che mi ha buttato fuori rotta è stato un cortese suggerimento di ReSharper sul metodo di attesa che supporta l'annullamento;) Potresti voler includere un tentativo di cattura nell'esempio, in quanto genererà un'operazione OperationCancelledException, che all'inizio non riuscivo a capire
Siewers

22

C # 7.1 (utilizzando vs 2017 aggiornamento 3) introduce asincrono principale

Tu puoi scrivere:

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

Per maggiori dettagli Serie C # 7, Parte 2: Async Main

Aggiornare:

È possibile che venga visualizzato un errore di compilazione:

Il programma non contiene un metodo 'Main' statico adatto per un punto di ingresso

Questo errore è dovuto al fatto che vs2017.3 è configurato per impostazione predefinita come c # 7.0 e non c # 7.1.

È necessario modificare esplicitamente l'impostazione del progetto per impostare le funzionalità di c # 7.1.

È possibile impostare c # 7.1 con due metodi:

Metodo 1: utilizzando la finestra delle impostazioni del progetto:

  • Apri le impostazioni del tuo progetto
  • Seleziona la scheda Genera
  • Fai clic sul pulsante Avanzate
  • Seleziona la versione desiderata Come mostrato nella figura seguente:

inserisci qui la descrizione dell'immagine

Metodo2: modifica manualmente il gruppo di proprietà di .csproj

Aggiungi questa proprietà:

    <LangVersion>7.1</LangVersion>

esempio:

    <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 stai usando C # 7.1 o versioni successive, seleziona la risposta di nawfal e modifica il tipo di ritorno del tuo metodo Main in Tasko Task<int>. Se tu non sei:

  • Chiedi async Task MainAsync a Johan come ha detto .
  • Chiamalo .GetAwaiter().GetResult()per catturare l'eccezione sottostante come ha detto do0g .
  • Supporta la cancellazione come ha detto Cory .
  • Un secondo CTRL+Cdovrebbe terminare immediatamente il processo. (Grazie binki !)
  • Gestisci OperationCancelledException: restituisce un codice di errore appropriato.

Il codice finale è simile a:

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
Molti bei programmi annulleranno CancelKeyPress solo la prima volta, quindi se si preme ^ C una volta si ottiene un arresto grazioso ma se si è impazienti un secondo ^ C termina in modo sfortunato. Con questa soluzione, è necessario uccidere manualmente il programma se non riesce a rispettare CancelToken perché e.Cancel = trueincondizionato.
binki

19

Non ne ho ancora avuto molto bisogno, ma quando ho usato l'applicazione console per i test rapidi e ho richiesto l'asincronizzazione, l'ho risolto in questo modo:

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

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

Questo esempio funzionerà in modo errato nel caso in cui sia necessario pianificare l'attività nel contesto corrente e quindi attendere (ad esempio, si può dimenticare di aggiungere ConfigureAwait (false), quindi il metodo di ritorno verrà programmato nel thread principale, che è nella funzione Wait ). Poiché il thread corrente è in stato di attesa, riceverai un deadlock.
Manushin Igor,

6
Non è vero, @ManushinIgor. Almeno in questo banale esempio, non è SynchronizationContextassociato al thread principale. Quindi non si bloccherà perché anche senza ConfigureAwait(false), tutte le continuazioni verranno eseguite sul threadpool.
Andrew Arnott,


4

In Main prova a cambiare la chiamata in GetList in:

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

4

Quando è stata introdotta la C # 5 CTP, certamente potrebbe contrassegnare principale con async... anche se non era una buona idea per farlo. Credo che questo sia stato modificato dal rilascio di VS 2013 per diventare un errore.

A meno che tu non abbia avviato altri thread in primo piano , il tuo programma verrà chiuso al Maintermine, anche se è stato avviato un lavoro in background.

Cosa stai davvero cercando di fare? Nota che il tuo GetList()metodo in realtà non ha bisogno di essere asincrono al momento - sta aggiungendo un livello aggiuntivo senza motivo reale. È logicamente equivalente a (ma più complicato di):

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

2
Jon, voglio ottenere gli elementi nell'elenco in modo asincrono, quindi perché l'asincrono non è appropriato su quel metodo GetList? È perché devo raccogliere gli elementi nell'elenco asincroni e non l'elenco stesso? Quando provo a contrassegnare il metodo Main con asincrono ottengo un "non contiene un metodo Main statico ..."
danielovich,

@danielovich: cosa DownloadTvChannels()restituisce? Presumibilmente restituisce un Task<List<TvChannel>>vero? In caso contrario, è improbabile che tu possa aspettarlo. (Possibile, dato il modello del cameriere, ma improbabile.) Per quanto riguarda il Mainmetodo - deve ancora essere statico ... forse hai sostituito il staticmodificatore con il asyncmodificatore?
Jon Skeet,

sì, restituisce un'attività <..> come hai detto. Non importa come provo a mettere asincrono nella firma del metodo principale che genera un errore. Sono seduto sui bit di anteprima VS11!
danielovich,

@danielovich: anche con un tipo di ritorno vuoto? Giusto public static async void Main() {}? Ma se DownloadTvChannels()restituisce già a Task<List<TvChannel>>, presumibilmente è già asincrono, quindi non è necessario aggiungere un altro livello. Vale la pena capirlo attentamente.
Jon Skeet,

1
@nawfal: guardando indietro, penso che sia cambiato prima che VS2013 fosse rilasciato. Non sono sicuro se C # 7 cambierà questo ...
Jon Skeet il

4

La versione più recente di C # - C # 7.1 consente di creare un'app console asincrona. Per abilitare C # 7.1 nel progetto, devi aggiornare il tuo VS ad almeno 15,3 e cambiare la versione di C # su C# 7.1o C# latest minor version. Per fare ciò, vai su Proprietà progetto -> Crea -> Avanzate -> Versione lingua.

Successivamente, funzionerà il seguente codice:

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

3

Su MSDN, la documentazione per il metodo Task.Run (Azione) fornisce questo esempio che mostra come eseguire un metodo in modo asincrono da 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

Nota questa affermazione che segue l'esempio:

Gli esempi mostrano che l'attività asincrona viene eseguita su un thread diverso rispetto al thread dell'applicazione principale.

Quindi, se invece vuoi che l'attività venga eseguita sul thread principale dell'applicazione, vedi la risposta di @StephenCleary .

E per quanto riguarda il thread su cui viene eseguita l'attività, nota anche il commento di Stephen sulla sua risposta:

È possibile utilizzare un semplice Waito Result, e non c'è niente di sbagliato in questo. Tuttavia, tieni presente che esistono due differenze importanti: 1) tutte le asynccontinuazioni vengono eseguite sul pool di thread anziché sul thread principale e 2) eventuali eccezioni sono racchiuse in un AggregateException.

(Vedi Gestione delle eccezioni (Task Parallel Library) per come incorporare la gestione delle eccezioni per gestire un AggregateException.)


Infine, su MSDN dalla documentazione per il metodo Task.Delay (TimeSpan) , questo esempio mostra come eseguire un'attività asincrona che restituisce un valore:

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

Nota che invece di passare un delegatea Task.Run, puoi invece passare una funzione lambda in questo modo:

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

1

Per evitare il blocco quando si chiama una funzione da qualche parte nello stack di chiamate che tenta di ricollegare il thread corrente (che è bloccato in un'attesa), è necessario effettuare le seguenti operazioni:

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

(il cast è richiesto solo per risolvere l'ambiguità)


Grazie; Task.Run non causa il deadlock di GetList (). Aspetta, questa risposta ha più voti ...
Stefano d'Antonio,

1

Nel mio caso avevo un elenco di lavori che volevo eseguire in modo asincrono dal mio metodo principale, lo stavo usando in produzione da un po 'di tempo e funziona bene.

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 ...
}
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.