Не удается указать модификатор "async" в методе "Main" консольного приложения

Я новичок в асинхронном программировании с async модификатор. Я пытаюсь понять, как убедиться, что мой Main Метод консольного приложения на самом деле работает асинхронно.

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

Я знаю, что это не работает асинхронно "сверху". Поскольку невозможно указать async модификатор на Main метод, как я могу запустить код внутри main асинхронно?

20 ответов

Решение

Как вы обнаружили, в VS11 компилятор запрещает async Main метод. Это было разрешено (но никогда не рекомендуется) в VS2010 с Async CTP.

У меня есть недавние сообщения в блоге об асинхронных /await и асинхронных консольных программах в частности. Вот некоторая справочная информация из вступительного поста:

Если "ожидающий" видит, что ожидаемое еще не завершено, он действует асинхронно. Он сообщает ожидаемому запускать оставшуюся часть метода после его завершения, а затем возвращается из асинхронного метода. Await также будет захватывать текущий контекст, когда он передает оставшуюся часть метода ожидающему.

Позже, когда ожидаемое завершится, оно выполнит оставшуюся часть асинхронного метода (в захваченном контексте).

Вот почему это проблема в консольных программах с async Main:

Помните из нашего вступительного поста, что асинхронный метод вернется к своему вызывающему до завершения. Это прекрасно работает в приложениях пользовательского интерфейса (метод просто возвращается в цикл событий пользовательского интерфейса) и приложениях ASP.NET (метод возвращает поток, но поддерживает запрос в действии). Это не очень хорошо работает с консольными программами: Main возвращается в ОС, поэтому ваша программа завершает работу.

Одним из решений является предоставление собственного контекста - "основного цикла" для вашей консольной программы, который является асинхронным.

Если у вас есть машина с Async CTP, вы можете использовать GeneralThreadAffineContext из Мои документы \Microsoft Visual Studio Async CTP\ Образцы (C# Testing) Модульное тестирование \ AsyncTestUtilities. Кроме того, вы можете использовать AsyncContext из моего пакета Nito.AsyncEx NuGet.

Вот пример использования AsyncContext; GeneralThreadAffineContext имеет почти одинаковое использование:

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

В качестве альтернативы, вы можете просто заблокировать основной поток консоли, пока ваша асинхронная работа не будет завершена:

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

Обратите внимание на использование GetAwaiter().GetResult(); это позволяет избежать AggregateException упаковка, которая происходит, если вы используете Wait() или же Result,

Обновление, 2017-11-30: Начиная с Visual Studio 2017, обновление 3 (15.3), язык теперь поддерживает async Main - пока он возвращается Task или же Task<T>, Теперь вы можете сделать это:

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

Семантика выглядит так же, как GetAwaiter().GetResult() стиль блокировки основного потока. Однако для C# 7.1 спецификации языка пока нет, так что это только предположение.

Вы можете решить это с помощью этой простой конструкции:

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

Это поместит все, что вы делаете, в ThreadPool, где вы этого хотите (поэтому другие задачи, которые вы запускаете / ожидаете, не пытаются присоединиться к потоку, которые они не должны), и ждут, пока все будет сделано, прежде чем закрывать консольное приложение. Нет необходимости в специальных петлях или наружных конечностях.

Изменить: включить решение Эндрю для необъяснимых исключений.

Вы можете сделать это без необходимости использования внешних библиотек, выполнив следующие действия:

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

В C# 7.1 вы сможете сделать правильную асинхронную Main. Соответствующие подписи для Main метод был расширен до:

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

Например, вы могли бы делать:

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

Во время компиляции асинхронный метод точки входа будет переведен в вызов GetAwaitor().GetResult(),

Подробности: https://blogs.msdn.microsoft.com/mazhou/2017/05/30/c-7-series-part-2-async-main

РЕДАКТИРОВАТЬ:

Чтобы включить функции языка C# 7.1, вам нужно щелкнуть правой кнопкой мыши по проекту и нажать "Свойства", а затем перейти на вкладку "Сборка". Там нажмите расширенную кнопку внизу:

введите описание изображения здесь

В раскрывающемся меню языковой версии выберите "7.1" (или любое более высокое значение):

введите описание изображения здесь

По умолчанию используется "последняя основная версия", которая оценивает (на момент написания этой статьи) C# 7.0, который не поддерживает асинхронное main в консольных приложениях.

Я добавлю важную функцию, которую пропустили все остальные ответы: отмена.

Одна из важных вещей в TPL - поддержка отмены, а в консольные приложения встроен метод отмены (CTRL+C). Это очень просто связать их вместе. Вот как я структурирую все свои асинхронные консольные приложения:

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

C# 7.1 (с использованием vs 2017 года обновление 3) вводит основной асинхронный

Ты можешь написать:

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

Для получения дополнительной информации C# 7 Series, часть 2: Async Main

Обновить:

Вы можете получить ошибку компиляции:

Программа не содержит статического метода Main, подходящего для точки входа

Эта ошибка связана с тем, что vs2017.3 по умолчанию настроен как C# 7.0, а не C# 7.1.

Вы должны явно изменить настройки вашего проекта, чтобы установить функции C# 7.1.

Вы можете установить C# 7.1 двумя способами:

Способ 1. Использование окна настроек проекта:

  • Откройте настройки вашего проекта
  • Выберите вкладку Build
  • Нажмите кнопку Дополнительно
  • Выберите нужную версию, как показано на следующем рисунке:

Метод 2: Изменить PropertyGroup.csproj вручную

Добавьте это свойство:

    <LangVersion>7.1</LangVersion>

пример:

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

Если вы используете C# 7.1 или более позднюю версию, перейдите к ответу nawfal и просто измените тип возврата вашего метода Main на Task или же Task<int>, Если вы не:

  • Есть async Task MainAsync как сказал Йохан.
  • Назови его .GetAwaiter().GetResult() чтобы поймать основное исключение, как сказал do0g.
  • Поддержка отмены, как сказал Кори.
  • Второй CTRL+C следует немедленно прекратить процесс. (Спасибо Бинки!)
  • Справиться OperationCancelledException - вернуть соответствующий код ошибки.

Окончательный код выглядит так:

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

В этом пока не было особой необходимости, но когда я использовал консольное приложение для быстрых тестов и требовал асинхронности, я просто решил это следующим образом:

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

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

Для асинхронного вызова задачи из Main используйте

  1. Task.Run() для.NET 4.5

  2. Task.Factory.StartNew () для.NET 4.0 (может потребоваться библиотека Microsoft.Bcl.Async для асинхронных и ожидающих ключевых слов)

Подробности: http://blogs.msdn.com/b/pfxteam/archive/2011/10/24/10229468.aspx

Новейшая версия C# - C# 7.1 позволяет создавать асинхронные консольные приложения. Чтобы включить C# 7.1 в проекте, вы должны обновить VS до версии не ниже 15.3 и изменить версию C# на C# 7.1 или же C# latest minor version, Для этого перейдите в Свойства проекта -> Сборка -> Дополнительно -> Языковая версия.

После этого будет работать следующий код:

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

В Main попробуйте изменить вызов GetList на:

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

Когда был представлен CTP CTP, вы, конечно, могли пометить Main как async... хотя это было вообще не очень хорошая идея. Я полагаю, что это было изменено выпуском VS 2013, чтобы стать ошибкой.

Если вы не запустили какие-либо другие потоки переднего плана, ваша программа завершит работу, когда Main завершает, даже если он начал некоторую фоновую работу.

Что ты на самом деле пытаешься сделать? Обратите внимание, что ваш GetList() метод действительно не должен быть асинхронным в данный момент - он добавляет дополнительный слой без какой-либо реальной причины. Это логически эквивалентно (но сложнее чем):

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

As of C# 7.1 the following signatures are valid for the Main method.

      public static void Main() { }
public static int Main() { }
public static void Main(string[] args) { }
public static int Main(string[] args) { }
public static async Task Main() { }
public static async Task<int> Main() { }
public static async Task Main(string[] args) { }
public static async Task<int> Main(string[] args) { }

So, now you can do async/await

      static async Task Main(string[] args)
{
    Console.WriteLine("Hello Asyn Main method!");
    await Task.Delay(200);
}

В моем случае у меня был список заданий, которые я хотел запускать в асинхронном режиме из своего основного метода, уже давно использовал его в работе и работает отлично.

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

В MSDN документация по методу Task.Run (Action) предоставляет этот пример, который показывает, как запустить метод асинхронно из 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

Обратите внимание на это утверждение, которое следует за примером:

Примеры показывают, что асинхронная задача выполняется в потоке, отличном от основного потока приложения.

Итак, если вместо этого вы хотите, чтобы задача выполнялась в главном потоке приложения, посмотрите ответ Stephen Cleary.

Что касается потока, в котором выполняется задача, также обратите внимание на комментарий Стивена к его ответу:

Вы можете использовать простой Wait или же Resultи в этом нет ничего плохого. Но имейте в виду, что есть два важных различия: 1) все async продолжения выполняются в пуле потоков, а не в основном потоке, и 2) любые исключения помещаются в AggregateException,

(См. Обработка исключений (Task Parallel Library) для получения информации о том, как включить обработку исключений для работы с AggregateException.)


Наконец, на MSDN из документации для метода Task.Delay (TimeSpan) этот пример показывает, как запустить асинхронную задачу, которая возвращает значение:

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

Обратите внимание, что вместо передачи delegate в Task.Runвместо этого вы можете передать лямбда-функцию следующим образом:

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

Чтобы избежать зависания при вызове функции где-нибудь в стеке вызовов, которая пытается повторно присоединиться к текущему потоку (который застрял в ожидании), вам необходимо сделать следующее:

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

(приведение требуется только для устранения неоднозначности)

Это гипотетически, но я думаю:

      static void Main(string[] args)
{
    var context = new Thread(() => /*do stuff*/);
    context.Start();
    context.Join();
}

Следующий код можно использовать для создания основного асинхронного файла. Я настроил его для использования длительных задач (подробнее здесь: https://learn.microsoft.com/en-us/dotnet/api/system.threading.tasks.taskcreationoptions?view=net-7.0 ).

Он также реализует токен отмены из приведенного выше ответа.

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

        try
        {
            var task = new Task<int>(
                () => MainAsync(args, cts.Token).GetAwaiter().GetResult(), 
                cts.Token,
                TaskCreationOptions.LongRunning //https://learn.microsoft.com/en-us/dotnet/api/system.threading.tasks.taskcreationoptions?view=net-7.0
            );
            task.Start();
            var exitCode =  task.GetAwaiter().GetResult();      
            /*Or this.*/
            //var exitCode = MainAsync(args, cts.Token).GetAwaiter().GetResult();
            return exitCode;// MainAsync(args, cts.Token).GetAwaiter().GetResult();
        } 
        catch (OperationCanceledException ex)
        {
            Console.WriteLine(ex);
            return 1223; // Cancelled.
        }
        catch(Exception ex)
        {
            Console.WriteLine(ex);
            return -1;
        }
    }
    private static async Task<int> MainAsync(string[] args, CancellationToken cancellationToken)
    {
         await Something()
         return;
    }

В следующем примере я написал. Вы можете поиграть с maxDegreeOfParallelism и numberOfIteration, чтобы понять/увидеть, как выполняется задача. Хорошая отправная точка для изучения TPL!

      private static async Task<int> MainAsync(string[] args, CancellationToken cancellationToken)
    {
        
        var infos = new ConcurrentBag<Info>();
        var mySuperUselessService = new BigWorkload();

        int numberOfSecond = 1;
        int numberOfIteration = 25;     //Experiment with this
        int maxDegreeOfParallelism = 4; //Experiment with this

        var simulateWorkTime = TimeSpan.FromSeconds(numberOfSecond);
        var informations = Enumerable.Range(1, numberOfIteration)
            .Select(x => new Info() { Index = x });

        var count = informations.Count();
        var chunkNeeded = Math.Round(count / Convert.ToDecimal(maxDegreeOfParallelism), MidpointRounding.ToPositiveInfinity);

        var splashInfo = @$"
Press CTRL + C to cancel. 
Processing {count} items, maxDegreeOfParallelism set to {maxDegreeOfParallelism}.
But it will be bound by the core on the machine {Environment.ProcessorCount}. 
This operation should take ~{chunkNeeded * (numberOfSecond + 0.01m)}s
And will be starting test in 2s
";
        Console.WriteLine(splashInfo);
        await Task.Delay(TimeSpan.FromSeconds(2));

        var parralelOptions = new ParallelOptions() { MaxDegreeOfParallelism = maxDegreeOfParallelism, CancellationToken = cancellationToken};
        var stopwatch = new Stopwatch();
        stopwatch.Start();
        var forLoopTask = Parallel.ForEachAsync(informations, parralelOptions, async (info, token) =>
        {
            await mySuperUselessService.Simulate(simulateWorkTime, info);
            Console.WriteLine(info);
            infos.Add(info);


        });
        await forLoopTask;
        stopwatch.Stop();

        foreach (var grouped in infos.GroupBy(x => x.ManagedThreadId))
        {
            Console.WriteLine($"ThreadId: {grouped.Key}");
            foreach (var item in grouped)
            {
                Console.WriteLine($"\t Index: {item.Index} {item.TaskCurrentId}");
                
            }
        }
        Console.WriteLine($"NumberOfThread: {infos.GroupBy(x => x.ManagedThreadId).Count()}");
        Console.WriteLine($"Elasped: {stopwatch.ElapsedMilliseconds / 1000d}s");
        Console.WriteLine(splashInfo);

        return 0;
        
    }
class Program
{
     public static EventHandler AsyncHandler;
     static void Main(string[] args)
     {
        AsyncHandler+= async (sender, eventArgs) => { await AsyncMain(); };
        AsyncHandler?.Invoke(null, null);
     }

     private async Task AsyncMain()
     {
        //Your Async Code
     }
}

Не уверен, что это то, что вы ищете, но я хотел дождаться метода при загрузке. В итоге я использовал обработчик Main_Shown и сделал его асинхронным:

      private async void Main_Shown(object sender, EventArgs e)
{
   await myAsyncMethod();
}
Другие вопросы по тегам