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


445

Я новичок в асинхронном программировании с 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асинхронном режиме?


23
Это больше не относится к C # 7.1. Основные методы могут быть асинхронными
Василий Слюнаев

2
Вот объявление в блоге C # 7.1 . Смотрите раздел под названием Async Main .
Styfle

Ответы:


382

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

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

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

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

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

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

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

Если у вас есть машина с Async CTP, вы можете использовать GeneralThreadAffineContextиз Моих документов \ Microsoft Visual Studio Async CTP \ Samples (C # Testing) Unit 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, так что это только предположение.


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

2
У меня была реальная проблема с выяснением этого до этого (и вашего сообщения в блоге). На сегодняшний день это самый простой способ решения этой проблемы, и вы можете установить пакет в консоли nuget с помощью всего лишь «install-package Nito.Asyncex», и все готово.
Константин К

1
@StephenCleary: Спасибо за быстрый ответ, Стивен. Я не понимаю, почему кто- то не хочет, чтобы отладчик ломался, когда выдается исключение. Если я отлаживаюсь и запускаю исключение с нулевой ссылкой, переход к некорректной строке кода представляется предпочтительным. VS работает так же «из коробки» для синхронного кода, но не для асинхронного / ожидающего.
Грег

6
В C # 7.1 теперь есть асинхронная магистраль, возможно, стоит добавить к вашему великолепному ответу
@StephenCleary

3
Если вы используете версию C # 7.1 в VS 2017, мне нужно было убедиться, что проект настроен на использование последней версии языка, добавив <LangVersion>latest</LangVersion>в файл csproj, как показано здесь .
Лиам

359

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

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

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

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


3
Этот подход очень очевиден, но имеет тенденцию оборачивать исключения, поэтому я сейчас ищу лучший способ.
Абатищев

2
@abatishchev Вы должны использовать try / catch в своем коде, по крайней мере, внутри Task.Run, если не более детально, не позволяя исключениям всплывать в Task. Вы избежите проблемы подведения итогов, разместив try / catch вокруг вещей, которые могут потерпеть неудачу.
Крис Москини

54
Если вы замените Wait()на, GetAwaiter().GetResult()вы избежите AggregateExceptionобертку, когда вещи бросают.
Эндрю Арнотт

7
Это как async mainвводится в C # 7.1, на момент написания этой статьи.
user9993

@ user9993 Согласно этому предложению , это не совсем так.
Синджай

90

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

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
Имейте в виду, что getListTask.Resultэто также блокирующий вызов, и поэтому приведенный выше код может быть написан без Task.WaitAll(getListTask).
do0g

27
Кроме того, если GetListthrows вам придется поймать AggregateExceptionи опросить его исключения, чтобы определить фактическое выброшенное исключение. Вы можете, однако, вызов , GetAwaiter()чтобы получить TaskAwaiterдля Task, и вызов GetResult()на что, то есть var list = getListTask.GetAwaiter().GetResult();. При получении результата от TaskAwaiter(также блокирующего вызова) любые сгенерированные исключения не будут помещены в AggregateException.
Do0g

1
.GetAwaiter (). GetResult был ответ, который мне был нужен. Это прекрасно работает для того, что я пытался сделать. Я, вероятно, буду использовать это и в других местах.
Deathstalker

78

В 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 в консольных приложениях.


2
ПОЭТОМУ это доступно в Visual Studio 15.3 и выше, которая в настоящее время доступна в виде бета-версии / предварительного просмотра здесь: visualstudio.com/vs/preview
Махмуд Аль-Кудси

Подождите минуту ... У меня установлена ​​полностью обновленная установка, и мой последний вариант - 7.1 ... Как вы получили 7.2 уже в мае?

Может ответ был мой. Редактирование в октябре было кем-то другим, когда я думаю, что 7.2 (превью?), Возможно, был выпущен.
nawfal

1
Заголовок - проверьте, что это на всех конфигах, а не только отладка, когда вы делаете это!
user230910

1
@ user230910 спасибо. Один из самых странных решений команды c #.
Nawfal

74

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

Одна из важных вещей в 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)
{
    ...
}

Должен ли токен отмены быть передан Wait()также?
Сиверс

5
Нет, потому что вы хотите, чтобы асинхронный код мог корректно обрабатывать отмену. Если вы передадите его в Wait(), он не будет ждать завершения асинхронного кода - он перестанет ждать и немедленно завершит процесс.
Кори Нельсон

Вы уверены, что? Я только что попробовал, и кажется, что запрос на отмену обрабатывается на самом глубоком уровне, даже когда Wait()метод передается с тем же маркером. То, что я пытаюсь сказать, это то, что это не имеет никакого значения.
Siewers

4
Я уверен. Вы хотите отменить саму операцию, а не ждать ее завершения. Если вы не заботитесь о завершении кода очистки или его результате.
Кори Нельсон

1
Да, я думаю, что понял, просто это не имело никакого значения в моем коде. Еще одна вещь, которая выбила меня из курса, была вежливая подсказка ReSharper о методе ожидания, поддерживающем отмену;) Возможно, вы захотите включить в пример
ловушку try

22

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>    

20

Если вы используете 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.
}

1
Многие хорошие программы отменяют CancelKeyPress только в первый раз, так что если вы нажмете ^ C, как только вы получите плавное завершение работы, но если вы нетерпеливы, секунда ^ C завершится без изящества. С этим решением вам нужно будет вручную убить программу, если она не сможет выполнить CancellationToken, потому что e.Cancel = trueэто безоговорочно.
Бинки

19

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

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

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

Этот пример будет работать неправильно, если вам нужно запланировать задачу в текущем контексте, а затем ждать ее (например, вы можете забыть добавить ConfigureAwait (false), поэтому метод возврата будет запланирован в основной поток, который находится в функции Wait ). Поскольку текущий поток находится в состоянии ожидания, вы получите тупик.
Манушин Игорь

6
Не правда, @ManushinIgor. По крайней мере, в этом тривиальном примере нет SynchronizationContextсвязи с основным потоком. Таким образом, он не блокируется, потому что даже без ConfigureAwait(false)этого все продолжения будут выполняться в пуле потоков.
Эндрю Арнотт



4

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

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

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

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

2
Джон, я хочу получить элементы списка асинхронно, так почему асинхронность не подходит для этого метода GetList? Это потому, что мне нужно собирать элементы списка асинхронно, а не сам список? Когда я пытаюсь пометить метод Main с помощью async, я получаю «не содержит статический метод Main ...»
Даниелович,

@danielovich: что DownloadTvChannels()возвращает? Предположительно это возвращает Task<List<TvChannel>>не так ли? Если нет, то вряд ли вы сможете его дождаться. (Возможно, учитывая модель awaiter, но маловероятно.) Что касается Mainметоды - он по- прежнему должен быть статическим ... вы заменить на staticмодификатор с asyncмодификатором возможно?
Джон Скит

да, он возвращает задание <..>, как вы сказали. Независимо от того, как я пытаюсь поставить async в сигнатуру метода Main, он выдает ошибку. Я сижу на битах предварительного просмотра VS11!
Даниелович

@danielovich: Даже с пустым типом возврата? Просто public static async void Main() {}? Но если он DownloadTvChannels()уже возвращает a Task<List<TvChannel>>, предположительно, он уже асинхронный - поэтому вам не нужно добавлять другой слой. Это стоит понять внимательно.
Джон Скит

1
@nawfal: Оглядываясь назад, я думаю, что все изменилось до выхода VS2013. Не уверен, что C # 7 изменит это ...
Джон Скит

4

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

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

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

3

В 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

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

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

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

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

Вы можете использовать простой Waitили Result, и в этом нет ничего плохого. Но имейте asyncв виду, что есть два важных различия: 1) все продолжения выполняются в пуле потоков, а не в основном потоке, и 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;
        });

1

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

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

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


Спасибо; Task.Run не вызывает тупик GetList (). Подождите, этот ответ должен иметь больше голосов ...
Стефано д'Антонио

1

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

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 ...
}
Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.