Неможливо вказати модифікатор '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 .
стифл

Відповіді:


382

Як ви виявили, у VS11 компілятор забороняє async Mainметод. Це було дозволено (але ніколи не рекомендується) у VS2010 за допомогою CTP Async.

У мене є останні повідомлення в блозі про програми асинхронізації / очікування та зокрема асинхронних консольних програм . Ось основна інформація з вступу:

Якщо "в очікуванні" бачить, що очікуване не завершилося, то воно діє асинхронно. Він повідомляє очікуваному запустити залишок методу після його завершення, а потім повертається з методу async. Очікування також захопить поточний контекст, коли він передасть залишок методу очікуваному.

Пізніше, коли очікуване завершиться, воно виконає решту методу асинхронізації (у захопленому контексті).

Ось чому це проблема в консольних програмах із async Main:

Згадайте з нашого вступного допису, що метод асинхронізації повернеться до свого виклику до його завершення. Це прекрасно працює в програмах UI (метод просто повертається до циклу подій UI) та додатках ASP.NET (метод повертається з потоку, але підтримує запит живим). Це не так добре виходить для консольних програм: Main повертається до ОС - значить, програма виходить з програми.

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

Якщо у вас є машина з CTP Async, ви можете використовувати GeneralThreadAffineContextв розділі Мої документи \ Microsoft Visual Studio Async CTP \ Samples (тестування C #) Тестування блоку \ 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 Update 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, і немає нічого поганого. Але майте на увазі, що є дві важливі відмінності: 1) всі asyncпродовження виконуються на пулі потоків, а не на основному потоці, і 2) будь-які винятки загортаються у AggregateException.
Стівен Клірі

2
У мене виникла справжня проблема, як з'ясувати це до цього (і вашого допису в блозі). Це, безумовно, найпростіший спосіб вирішити цю проблему, і ви можете встановити пакунок на нульовій консолі, просто "встановивши пакет Nito.Asyncex", і ви закінчили.
КостянтинК

1
@StephenCleary: Дякую за швидку відповідь Стівен. Я не розумію, чому хтось не хотів би, щоб налагоджувач зламався, коли викинуто виняток. Якщо я налагоджую помилку і перетинаю нульове виняткове посилання, перехід безпосередньо до рядка коду, що порушив, здається кращим. VS працює так "поза коробкою" для синхронного коду, але не для асинхронізації / очікування.
Грег

6
C # 7.1 тепер має основну функцію асинхронізації, можливо, варто додати свою чудову відповідь, @StephenCleary github.com/dotnet/csharplang/blob/master/proposals/csharp-7.1/…
Mafii

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 там, де ви цього хочете (тому інші завдання, які ви починаєте / очікуєте, не намагайтеся знову приєднатись до теми, до яких вони не повинні), і зачекайте, поки все буде зроблено, перш ніж закрити додаток Console. Не потрібно спеціальних петель або зовнішніх петель.

Редагувати. Включіть рішення Ендрю для виняткових винятків.


3
Цей підхід є дуже очевидним, але, як правило, містить винятки, тому я шукаю кращого способу.
абатищев

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

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
Крім того, якщо GetListкидки вам доведеться зловити 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, яка не підтримує функцію async main у консольних програмах.


2
FWIW це доступно у Visual Studio 15.3 та новіших версій
Махмуд Аль-Кудсі

Зачекайте хвилину ... Я запускаю повністю оновлену установку, і мій останній варіант - 7.1 ... як би ви отримали 7,2 вже в травні?

Моя відповідь була моєю. Жовтня редакція була кимось іншим на той момент, я думаю, що 7.2 (попередній перегляд?) Може бути звільнений.
nawfal

1
Heads up - перевірте, чи він у всіх конфігураціях, а не просто налагодження, коли ви це робите!
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(), він не чекатиме закінчення асинхронного коду - він зупинить очікування і негайно закінчить процес.
Cory Nelson

Ви впевнені в цьому? Я щойно спробував це, і, схоже, запит на скасування обробляється на найглибшому рівні, навіть коли Wait()метод передається тим же маркером. Я намагаюся сказати, що це, здається, не має ніякого значення.
глядачі

4
Я впевнений. Ви хочете скасувати оп, а не чекати завершення оп. Якщо ви не піклуєтеся про завершення чищення коду чи його результат.
Cory Nelson

1
Так, я думаю, що я це зрозумів, це, схоже, не змінило мого коду. Інша річ, яка мене відкинула, - це ввічливий натяк ReSharper про спосіб очікування, що підтримує скасування;) Ви можете захотіти включити спробу вловлювання в приклад, оскільки це викине OperationCancegledException, якого я не міг з'ясувати спочатку
Siewers

22

C # 7.1 (використовуючи відновлення 3 для 2017 року) вводить функцію async main

Ви можете написати:

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

Докладніше: Серія C # 7, Частина 2: Асинхронна головна

Оновлення:

Ви можете отримати помилку компіляції:

Програма не містить статичного методу "Main", придатного для точки входу

Ця помилка пов'язана з тим, що vs2017.3 налаштовано за замовчуванням як c # 7.0, а не c # 7.1.

Вам слід явно змінити налаштування вашого проекту, щоб встановити функції # 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 і просто змініть тип повернення вашого основного методу на 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 одного разу, ви отримаєте витончене вимкнення, але якщо ви нетерплячі, секунда ^ С закінчується невміло. За допомогою цього рішення вам потрібно буде вручну вбити програму, якщо вона не вшановує 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)зайде в глухий кут, тому що навіть без цього всі продовження виконуватимуться на нитковій пулі.
Ендрю Арнотт

7

Для асинхронного виклику завдання від 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


4

У головному спробуйте змінити виклик GetList на:

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

4

Коли було введено CTP C # 5, ви, безумовно, можете позначити Main з async... хоча це, як правило, не було гарною ідеєю. Я вважаю, що це було змінено випуском VS 2013, щоб стати помилкою.

Якщо ви не запустили будь-які інші потоки переднього плану , програма завершиться, коли Mainзавершиться, навіть якщо вона розпочала роботу з фоном.

Що ви насправді намагаєтесь зробити? Зауважте, що ваш GetList()метод справді не потрібно асинхронізувати - це додавання додаткового шару без реальних причин. Це логічно еквівалентно (але складніше, ніж):

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

2
Джон, я хочу отримати елементи в списку асинхронно, тож чому асинхронізація не підходить для цього методу GetList? Це тому, що мені потрібно зібрати елементи зі списку асинхронізацію ', а не сам список? Коли я намагаюся позначити метод Main з асинхронізуванням, я отримую "не містить статичного основного методу ..."
danielovich

@danielovich: Що DownloadTvChannels()повертається? Імовірно, він повертає Task<List<TvChannel>>чи не так? Якщо ні, то навряд чи ви зможете його чекати. (Можливо, з огляду на модель awaiter, але малоймовірно.) Що стосується Mainметоди - він по- , як і раніше повинен бути статичним ... ви замінити на staticмодифікатор з asyncмодифікатором можливо?
Джон Скіт

так, він повертає Завдання <..>, як ви сказали. Незалежно від того, як я намагаюся вписати async в підпис основного методу, він видає помилку. Я сиджу на VS11 бітах попереднього перегляду!
danielovich

@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.1або C# latest minor version. Для цього перейдіть до Властивості проекту -> Збірка -> Додатково -> Мовна версія.

Після цього буде діяти наступний код:

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

3

У MSDN документація для методу Task.Run (дія) надає цей приклад, який показує, як запустити метод асинхронно з 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, і немає нічого поганого. Але майте на увазі, що є дві важливі відмінності: 1) всі asyncпродовження виконуються на пулі потоків, а не на основному потоці, і 2) будь-які винятки загортаються у AggregateException.

(Див. Посібник із винятками (Бібліотека паралельних завдань) про те, як включити обробку виключень для боротьби з 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

Щоб уникнути зависання, коли ви викликаєте функцію десь у стеку викликів, яка намагається знову приєднатись до поточного потоку (який застряг у Wait), вам потрібно зробити наступне:

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.