Impossible de spécifier le modificateur 'async' sur la méthode 'Main' d'une application console

445

Je suis nouveau dans la programmation asynchrone avec le asyncmodificateur. J'essaie de comprendre comment m'assurer que ma Mainméthode d'une application console s'exécute réellement de manière asynchrone.

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

Je sais que cela ne fonctionne pas de manière asynchrone à partir du "haut". Puisqu'il n'est pas possible de spécifier le asyncmodificateur sur la Mainméthode, comment puis-je exécuter le code dans mainasynchrone?

danielovich
la source
23
Ce n'est plus le cas en C # 7.1. Les principales méthodes peuvent être asynchrones
Vasily Sliounaiev
2
Voici l' annonce de l'article de blog C # 7.1 . Voir la section intitulée Async Main .
styfle du

Réponses:

382

Comme vous l'avez découvert, dans VS11, le compilateur interdira une async Mainméthode. Cela a été autorisé (mais jamais recommandé) dans VS2010 avec le CTP Async.

J'ai récemment publié des articles de blog sur async / wait et les programmes de console asynchrones en particulier. Voici quelques informations de base du post d'introduction:

Si "attendre" voit que l'attente n'est pas terminée, alors il agit de manière asynchrone. Il indique à l'attente d'exécuter le reste de la méthode lorsqu'elle se termine, puis revient de la méthode async. Await capturera également le contexte actuel lorsqu'il passera le reste de la méthode à l'attendu.

Plus tard, à la fin de l'attente, il exécutera le reste de la méthode async (dans le contexte capturé).

Voici pourquoi il s'agit d'un problème dans les programmes de console avec un async Main:

Rappelez-vous de notre post d'introduction qu'une méthode async retournera à son appelant avant d'être terminée. Cela fonctionne parfaitement dans les applications d'interface utilisateur (la méthode revient simplement à la boucle d'événement d'interface utilisateur) et les applications ASP.NET (la méthode renvoie le thread mais maintient la demande en vie). Cela ne fonctionne pas si bien pour les programmes de console: Main revient sur le système d'exploitation - donc votre programme se ferme.

Une solution consiste à fournir votre propre contexte - une «boucle principale» pour votre programme de console compatible asynchrone.

Si vous avez une machine avec le CTP Async, vous pouvez utiliser à GeneralThreadAffineContextpartir de Mes Documents \ Microsoft Visual Studio CTP Async \ Samples (test C #) Unit Testing \ AsyncTestUtilities . Alternativement, vous pouvez utiliser à AsyncContextpartir de mon package Nito.AsyncEx NuGet .

Voici un exemple utilisant AsyncContext; GeneralThreadAffineContexta une utilisation presque identique:

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

Alternativement, vous pouvez simplement bloquer le thread principal de la console jusqu'à ce que votre travail asynchrone soit terminé:

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

Notez l'utilisation de GetAwaiter().GetResult(); cela évite le AggregateExceptionwrapping qui se produit si vous utilisez Wait()ou Result.

Mise à jour, 2017-11-30: à partir de Visual Studio 2017 Update 3 (15.3), la langue prend désormais en charge un async Main- tant qu'elle renvoie Taskou Task<T>. Vous pouvez donc maintenant faire ceci:

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

La sémantique semble être la même que le GetAwaiter().GetResult()style de blocage du thread principal. Cependant, il n'y a pas encore de spécification de langage pour C # 7.1, donc ce n'est qu'une hypothèse.

Stephen Cleary
la source
30
Vous pouvez utiliser un simple Waitou Result, et il n'y a rien de mal à cela. Mais sachez qu'il existe deux différences importantes: 1) toutes les asynccontinuations s'exécutent sur le pool de threads plutôt que sur le thread principal, et 2) toutes les exceptions sont encapsulées dans un AggregateException.
Stephen Cleary
2
A eu un vrai problème à comprendre cela jusqu'à ce que cela (et votre article de blog). C'est de loin la méthode la plus simple pour résoudre ce problème, et vous pouvez installer le package dans la console nuget avec juste un "package d'installation Nito.Asyncex" et vous avez terminé.
ConstantineK
1
@StephenCleary: Merci pour la réponse rapide Stephen. Je ne comprends pas pourquoi quelqu'un ne voudrait pas que le débogueur se casse lorsqu'une exception est levée. Si je débogue et que je rencontre une exception de référence nulle, aller directement à la ligne de code incriminée semble préférable. VS fonctionne comme ça "prêt à l'emploi" pour le code synchrone, mais pas pour async / attente.
Greg
6
C # 7.1 a maintenant un principal asynchrone, cela pourrait valoir la peine d'ajouter à votre excellente réponse, @StephenCleary github.com/dotnet/csharplang/blob/master/proposals/csharp-7.1/…
Mafii
3
Si vous utilisez la version C # 7.1 dans VS 2017, je devais m'assurer que le projet était configuré pour utiliser la dernière version de la langue en l'ajoutant <LangVersion>latest</LangVersion>au fichier csproj, comme indiqué ici .
Liam
359

Vous pouvez résoudre ce problème avec cette construction simple:

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

Cela mettra tout ce que vous faites sur le ThreadPool où vous le souhaitez (de sorte que les autres tâches que vous démarrez / attendez n'essaient pas de rejoindre un thread, elles ne devraient pas), et attendez que tout soit fait avant de fermer l'application Console. Pas besoin de boucles spéciales ou de bibliothèques externes.

Edit: Incorporer la solution d'Andrew pour les exceptions non capturées.

Chris Moschini
la source
3
Cette approche est très évidente mais tend à encapsuler les exceptions, donc je cherche maintenant un meilleur moyen.
abatishchev
2
@abatishchev Vous devriez utiliser try / catch dans votre code, au moins à l'intérieur du Task.Run sinon plus granuleusement, sans laisser les exceptions flotter jusqu'à la Task. Vous éviterez le problème de fin en mettant try / catch autour des choses qui peuvent échouer.
Chris Moschini
54
Si vous remplacez Wait()par, GetAwaiter().GetResult()vous éviterez le AggregateExceptionwrapper lorsque les choses se lanceront.
Andrew Arnott
7
Voilà comment async mainest introduit dans C # 7.1, au moment de la rédaction de cet article.
user9993
@ user9993 Selon cette proposition , ce n'est pas tout à fait vrai.
Sinjai
90

Vous pouvez le faire sans avoir besoin de bibliothèques externes également en procédant comme suit:

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
la source
7
Gardez à l'esprit qu'il getListTask.Results'agit également d'un appel bloquant et que le code ci-dessus pourrait être écrit sans Task.WaitAll(getListTask).
do0g
27
De plus, si GetListvous lancez, vous devrez attraper un AggregateExceptionet interroger ses exceptions pour déterminer l'exception réelle levée. Vous pouvez, cependant, appeler GetAwaiter()pour obtenir le TaskAwaiterpour Task, et faire appel GetResult()à cela, c'est-à-dire var list = getListTask.GetAwaiter().GetResult();. Lors de l'obtention du résultat de TaskAwaiter(également un appel de blocage), les exceptions levées ne seront pas encapsulées dans un AggregateException.
do0g
1
.GetAwaiter (). GetResult était la réponse dont j'avais besoin. Cela fonctionne parfaitement pour ce que j'essayais de faire. Je vais probablement l'utiliser aussi dans d'autres endroits.
Deathstalker
78

En C # 7.1, vous pourrez faire un bon async Main . Les signatures appropriées pour la Mainméthode ont été étendues à:

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

Par exemple, vous pourriez faire:

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

Au moment de la compilation, la méthode du point d'entrée asynchrone sera traduite en appel GetAwaitor().GetResult().

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

ÉDITER:

Pour activer les fonctionnalités du langage C # 7.1, vous devez cliquer avec le bouton droit sur le projet et cliquer sur "Propriétés" puis aller dans l'onglet "Construire". Là, cliquez sur le bouton avancé en bas:

entrez la description de l'image ici

Dans le menu déroulant de la version linguistique, sélectionnez "7.1" (ou toute valeur supérieure):

entrez la description de l'image ici

La valeur par défaut est "la dernière version majeure" qui évaluerait (au moment d'écrire ces lignes) en C # 7.0, qui ne prend pas en charge async main dans les applications de console.

nawfal
la source
2
FWIW ceci est disponible dans Visual Studio 15.3 et supérieur, qui est actuellement disponible en version bêta / prévisualisée à partir d'ici: visualstudio.com/vs/preview
Mahmoud Al-Qudsi
Attendez une minute ... J'exécute une installation entièrement mise à jour et ma dernière option est la 7.1 ... comment avez-vous déjà obtenu la 7.2 en mai?
Peut répondre était la mienne. La modification d'octobre a été effectuée par quelqu'un d'autre au moment où je pense que la version 7.2 (préversion?) Pourrait avoir été publiée.
nawfal
1
Attention - vérifiez qu'il est sur toutes les configurations, pas seulement le débogage lorsque vous faites cela!
user230910
1
@ user230910 merci. L'un des choix les plus étranges de l'équipe c #.
nawfal
74

J'ajouterai une caractéristique importante que toutes les autres réponses ont ignorée: l'annulation.

L'une des grandes choses dans TPL est la prise en charge de l'annulation, et les applications console ont une méthode d'annulation intégrée (CTRL + C). C'est très simple de les lier ensemble. Voici comment je structure toutes mes applications de console asynchrone:

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
la source
Le jeton d'annulation doit-il également être transmis au Wait()?
Siewers
5
Non, car vous souhaitez que le code asynchrone puisse gérer correctement l'annulation. Si vous le transmettez à Wait(), il n'attendra pas la fin du code asynchrone - il arrêtera d'attendre et mettra immédiatement fin au processus.
Cory Nelson
Êtes-vous sûr de cela? Je viens de l'essayer, et il semble que la demande d'annulation soit traitée au niveau le plus profond, même lorsque la Wait()méthode reçoit le même jeton. Ce que j'essaie de dire, c'est que cela ne semble pas faire de différence.
Siewers
4
Je suis sûr. Vous voulez annuler l'opération elle-même, pas attendre la fin de l'opération. Sauf si vous ne vous souciez pas de la finition du code de nettoyage ou du résultat.
Cory Nelson
1
Oui, je pense que je comprends, cela ne semble pas faire de différence dans mon code. Une autre chose qui m'a dérouté était une astuce polie de ReSharper sur la méthode d'attente prenant en charge l'annulation;) Vous voudrez peut-être inclure une capture d'essai dans l'exemple, car cela lancera une OperationCancelledException, que je ne pouvais pas comprendre au début
Siewers
22

C # 7.1 (en utilisant vs 2017 update 3) présente async main

Tu peux écrire:

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

Pour plus de détails, série C # 7, partie 2: Async Main

Mise à jour:

Vous pouvez obtenir une erreur de compilation:

Le programme ne contient pas de méthode statique «principale» adaptée à un point d'entrée

Cette erreur est due au fait que vs2017.3 est configuré par défaut en tant que c # 7.0 et non c # 7.1.

Vous devez modifier explicitement le paramètre de votre projet pour définir les fonctionnalités c # 7.1.

Vous pouvez définir c # 7.1 par deux méthodes:

Méthode 1: à l'aide de la fenêtre des paramètres du projet:

  • Ouvrez les paramètres de votre projet
  • Sélectionnez l'onglet Build
  • Cliquez sur le bouton Avancé
  • Sélectionnez la version souhaitée, comme indiqué dans la figure suivante:

entrez la description de l'image ici

Méthode 2: modifier manuellement le PropertyGroup de .csproj

Ajouter cette propriété:

    <LangVersion>7.1</LangVersion>

exemple:

    <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
la source
20

Si vous utilisez C # 7.1 ou version ultérieure, utilisez la réponse du nawfal et changez simplement le type de retour de votre méthode Main en Taskou Task<int>. Si tu n'es pas:

  • Ayez un async Task MainAsync comme Johan l'a dit .
  • Appelez son .GetAwaiter().GetResult()pour attraper l'exception sous-jacente comme l'a dit do0g .
  • Annulation du support comme l'a dit Cory .
  • Une seconde CTRL+Cdevrait mettre fin au processus immédiatement. (Merci binki !)
  • Handle OperationCancelledException- retourne un code d'erreur approprié.

Le code final ressemble à:

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
la source
1
De nombreux programmes agréables n'annuleront la touche CancelKeyPress que la première fois, de sorte que si vous appuyez sur ^ C une fois, vous obtenez un arrêt gracieux, mais si vous êtes impatient, une seconde ^ C se termine sans grâce. Avec cette solution, vous devrez tuer manuellement le programme s'il ne respecte pas le CancellationToken car il e.Cancel = trueest inconditionnel.
binki
19

Je n'en ai pas encore eu besoin, mais quand j'ai utilisé une application console pour des tests rapides et asynchrone requis, je viens de le résoudre comme ceci:

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

    static async Task MainAsync(string[] args)
    {
        // Code here
    }
}
Johan Falk
la source
Cet exemple fonctionnera mal au cas où vous devriez planifier la tâche dans le contexte actuel et l'attendre (par exemple, vous pouvez oublier d'ajouter ConfigureAwait (false), donc la méthode de retour sera planifiée dans le thread principal, qui est dans la fonction Wait ). Étant donné que le thread actuel est en attente, vous recevrez un blocage.
Manushin Igor
6
Pas vrai, @ManushinIgor. Au moins dans cet exemple trivial, il n'y a aucun SynchronizationContextassocié au thread principal. Il ne ConfigureAwait(false)bloquera donc pas, car même sans , toutes les continuations s'exécuteront sur le pool de threads.
Andrew Arnott
4

Dans Main, essayez de changer l'appel à GetList pour:

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

Lorsque le C # 5 CTP a été introduit, vous avez certainement pu marquer principale avecasync ... même si ce n'était généralement pas une bonne idée de le faire. Je crois que cela a été changé par la sortie de VS 2013 pour devenir une erreur.

Sauf si vous avez démarré d'autres threads de premier plan , votre programme se fermera à la Mainfin, même s'il a commencé un travail en arrière-plan.

Qu'essayez-vous vraiment de faire? Notez que votre GetList()méthode n'a vraiment pas besoin d'être asynchrone pour le moment - elle ajoute une couche supplémentaire sans raison réelle. C'est logiquement équivalent à (mais plus compliqué que):

public Task<List<TvChannel>> GetList()
{
    return new GetPrograms().DownloadTvChannels();
}
Jon Skeet
la source
2
Jon, je veux obtenir les éléments de la liste de manière asynchrone, alors pourquoi l'async n'est pas approprié sur cette méthode GetList? Est-ce parce que j'ai besoin de collecter les éléments de la liste en mode asynchrone et non de la liste elle-même? Lorsque j'essaie de marquer la méthode Main avec async, j'obtiens "ne contient pas de méthode Main statique ..."
danielovich
@danielovich: Qu'est-ce que le DownloadTvChannels()retour? Vraisemblablement, il renvoie un Task<List<TvChannel>>n'est-ce pas? Sinon, il est peu probable que vous puissiez l'attendre. (Possible, étant donné le motif d'attente, mais peu probable.) Quant à la Mainméthode - elle doit toujours être statique ... avez-vous remplacé le staticmodificateur par le asyncmodificateur peut-être?
Jon Skeet
oui, il renvoie une tâche <..> comme vous l'avez dit. Peu importe comment j'essaie de mettre async dans la signature de la méthode Main, cela génère une erreur. Je suis assis sur les bits de prévisualisation VS11!
danielovich
@danielovich: Même avec un type de retour nul? Juste public static async void Main() {}? Mais si DownloadTvChannels()renvoie déjà un Task<List<TvChannel>>, il est probablement déjà asynchrone - vous n'avez donc pas besoin d'ajouter un autre calque. Cela vaut la peine de bien comprendre cela.
Jon Skeet
1
@nawfal: Avec le recul, je pense que cela a changé avant la sortie de VS2013. Je ne sais pas si C # 7 va changer ça ...
Jon Skeet
4

La dernière version de C # - C # 7.1 permet de créer une application console asynchrone. Pour activer C # 7.1 dans le projet, vous devez mettre à niveau votre VS vers au moins 15.3 et changer la version C # en C# 7.1ou C# latest minor version. Pour ce faire, accédez aux propriétés du projet -> Build -> Advanced -> Version linguistique.

Après cela, le code suivant fonctionnera:

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

Sur MSDN, la documentation de la méthode Task.Run (Action) fournit cet exemple qui montre comment exécuter une méthode de manière asynchrone à partir de 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

Notez cette déclaration qui suit l'exemple:

Les exemples montrent que la tâche asynchrone s'exécute sur un thread différent du thread d'application principal.

Donc, si vous souhaitez que la tâche s'exécute sur le thread principal de l'application, consultez la réponse de @StephenCleary .

Et en ce qui concerne le fil sur lequel la tâche s'exécute, notez également le commentaire de Stephen sur sa réponse:

Vous pouvez utiliser un simple Waitou Result, et il n'y a rien de mal à cela. Mais sachez qu'il existe deux différences importantes: 1) toutes les asynccontinuations s'exécutent sur le pool de threads plutôt que sur le thread principal, et 2) toutes les exceptions sont encapsulées dans un AggregateException.

(Voir Gestion des exceptions (bibliothèque parallèle de tâches) pour savoir comment incorporer la gestion des exceptions pour gérer un problème AggregateException.)


Enfin, sur MSDN à partir de la documentation de la méthode Task.Delay (TimeSpan) , cet exemple montre comment exécuter une tâche asynchrone qui renvoie une valeur:

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

Notez qu'au lieu de passer un delegateà Task.Run, vous pouvez plutôt passer une fonction lambda comme ceci:

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

Pour éviter le gel lorsque vous appelez une fonction quelque part dans la pile des appels qui tente de rejoindre le thread actuel (qui est bloqué dans une attente), vous devez procéder comme suit:

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

(le casting n'est nécessaire que pour résoudre l'ambiguïté)

Nathan Phillips
la source
Merci; Task.Run ne provoque pas le blocage de GetList (). Attendez, cette réponse devrait avoir plus de votes positifs
Stefano d'Antonio
1

Dans mon cas, j'avais une liste de tâches que je voulais exécuter en asynchrone à partir de ma méthode principale, j'utilisais cela en production depuis un certain temps et fonctionnait 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 ...
}
user_v
la source