Nie można określić modyfikatora „asynchronicznego” w metodzie „Main” aplikacji konsoli


445

Jestem nowy w programowaniu asynchronicznym z asyncmodyfikatorem. Próbuję dowiedzieć się, jak upewnić się, że moja Mainmetoda aplikacji konsolowej faktycznie działa asynchronicznie.

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

Wiem, że to nie działa asynchronicznie „od góry”. Ponieważ nie można określić asyncmodyfikatora Mainmetody, jak mogę uruchomić kod w ramach mainasynchronicznie?


23
Tak już nie jest w C # 7.1. Główne metody mogą być asynchroniczne
Wasilij Sliounaiev

2
Oto ogłoszenie na blogu w języku C # 7.1 . Zobacz sekcję Async Main .
styfle,

Odpowiedzi:


382

Jak odkryłeś, w VS11 kompilator nie zezwala na async Mainmetodę. Było to dozwolone (ale nigdy nie zalecane) w VS2010 z Async CTP.

Mam najnowsze posty na blogu dotyczące asynchronizacji / oczekiwania i w szczególności asynchronicznych programów konsolowych . Oto kilka podstawowych informacji z postu wprowadzającego:

Jeśli „oczekiwanie” zobaczy, że oczekiwane nie zostało zakończone, działa on asynchronicznie. Mówi oczekiwanemu o uruchomieniu pozostałej części metody po jej zakończeniu, a następnie powraca z metody asynchronicznej. Oczekuje również na przechwycenie bieżącego kontekstu, gdy przekaże pozostałą część metody do oczekiwanego.

Później, gdy oczekiwane zadanie się zakończy, wykona pozostałą część metody asynchronicznej (w przechwyconym kontekście).

Oto dlaczego jest to problem w programach konsoli z async Main:

Pamiętaj z naszego wpisu wstępnego, że metoda asynchroniczna powróci do swojego obiektu wywołującego, zanim zostanie zakończona. Działa to doskonale w aplikacjach interfejsu użytkownika (metoda po prostu wraca do pętli zdarzeń interfejsu użytkownika) i aplikacjach ASP.NET (metoda zwraca wątek, ale utrzymuje żądanie przy życiu). Nie działa tak dobrze w przypadku programów konsolowych: Main powraca do systemu operacyjnego - więc program się kończy.

Jednym z rozwiązań jest zapewnienie własnego kontekstu - „głównej pętli” dla programu konsoli, która jest kompatybilna z asynchronicznie.

Jeśli masz komputer z Async CTP, możesz korzystać GeneralThreadAffineContextz My Documents \ Microsoft Visual Studio Async CTP \ Samples (C # Testing) Testowanie jednostkowe \ AsyncTestUtilities . Alternatywnie możesz użyć AsyncContextz mojego pakietu Nito.AsyncEx NuGet .

Oto przykład użycia AsyncContext; GeneralThreadAffineContextma prawie identyczne zastosowanie:

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

Alternatywnie możesz po prostu zablokować główny wątek konsoli, dopóki praca asynchroniczna nie zostanie zakończona:

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

Zwróć uwagę na użycie GetAwaiter().GetResult(); pozwala to uniknąć AggregateExceptionowijania, które występuje, jeśli używasz Wait()lub Result.

Aktualizacja, 30.11.2017: Od wersji 3 programu Visual Studio 2017 (15.3) język obsługuje teraz async Main- o ile zwraca Tasklub Task<T>. Możesz teraz to zrobić:

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

Semantyka wydaje się być taka sama, jak GetAwaiter().GetResult()styl blokowania głównego wątku. Jednak nie ma jeszcze specyfikacji języka dla C # 7.1, więc jest to tylko przypuszczenie.


30
Państwo może użyć prostego Waitlub Result, i nie ma nic złego. Należy jednak pamiętać, że istnieją dwie ważne różnice: 1) wszystkie asynckontynuacje są uruchamiane w puli wątków, a nie w głównym wątku, oraz 2) wszelkie wyjątki są zawinięte w plik AggregateException.
Stephen Cleary

2
Miałem prawdziwy problem z rozwikłaniem tego do tego czasu (i twojego posta na blogu). Jest to zdecydowanie najłatwiejsza metoda rozwiązania tego problemu. Możesz zainstalować pakiet w konsoli nuget za pomocą „pakietu instalacyjnego Nito.Asyncex” i gotowe.
ConstantineK,

1
@StephenCleary: Dzięki za szybką odpowiedź Stephen. Nie rozumiem, dlaczego nikt nie chciałby, aby debugger się zepsuł, gdy zostanie zgłoszony wyjątek. Jeśli debuguję i natrafię na wyjątek odniesienia o wartości zerowej, preferowane jest przejście bezpośrednio do obraźliwej linii kodu. VS działa w ten sposób „po wyjęciu z pudełka” dla kodu synchronicznego, ale nie dla asynchronizacji / oczekiwania.
Greg,

6
C # 7.1 ma teraz asynchroniczną stronę główną, warto dodać do swojej świetnej odpowiedzi, @StephenCleary github.com/dotnet/csharplang/blob/master/propozycje/csharp-7.1/…
Mafii

3
Jeśli używasz wersji C # 7.1 w VS 2017, musiałem upewnić się, że projekt został skonfigurowany do używania najnowszej wersji języka, dodając <LangVersion>latest</LangVersion>do pliku csproj, jak pokazano tutaj .
Liam,

359

Możesz rozwiązać ten problem za pomocą tej prostej konstrukcji:

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

Spowoduje to umieszczenie wszystkiego, co robisz w ThreadPool tam, gdzie chcesz (więc inne zadania, które uruchamiasz / czekasz, nie próbują ponownie dołączyć do wątku, których nie powinny) i poczekają, aż wszystko się skończy, zanim zamkniesz aplikację Console. Nie ma potrzeby stosowania specjalnych pętli ani zewnętrznych bibliotek.

Edycja: uwzględnij rozwiązanie Andrew dla nieprzechwyconych wyjątków.


3
To podejście jest bardzo oczywiste, ale zwykle obejmuje wyjątki, dlatego szukam teraz lepszego sposobu.
abatishchev

2
@abatishchev Powinieneś używać try / catch w swoim kodzie, przynajmniej w Task.Run, jeśli nie bardziej szczegółowo, nie pozwalając wyjątkom unosić się do Task. Unikniesz problemu zakończenia, próbując złapać rzeczy, które mogą zawieść.
Chris Moschini

54
Jeśli zastąpić Wait()z GetAwaiter().GetResult()was będzie uniknąć AggregateExceptionopakowanie kiedy rzucać rzeczy.
Andrew Arnott,

7
Tak to async mainjest wprowadzane w C # 7.1, od tego pisania.
user9993

@ user9993 Według tej propozycji nie jest to do końca prawda.
Sinjai

90

Możesz to zrobić bez potrzeby korzystania z zewnętrznych bibliotek, wykonując następujące czynności:

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
Pamiętaj, że getListTask.Resultjest to również wywołanie blokujące, więc powyższy kod może zostać napisany bez Task.WaitAll(getListTask).
do0g

27
Ponadto, jeśli GetListwyrzucisz, będziesz musiał złapać AggregateExceptioni przesłuchać jego wyjątki, aby ustalić faktyczny zgłoszony wyjątek. Można jednak zadzwonić GetAwaiter(), aby uzyskać TaskAwaiterdla Taski rozmowa GetResult()na ten temat, tj var list = getListTask.GetAwaiter().GetResult();. Po uzyskaniu wyniku z TaskAwaiter(również wywołania blokującego) wszelkie zgłoszone wyjątki nie zostaną zawinięte w AggregateException.
do0g

1
.GetAwaiter (). GetResult była odpowiedzią, której potrzebowałem. To działa idealnie dla tego, co próbowałem zrobić. Prawdopodobnie użyję tego również w innych miejscach.
Deathstalker

78

W C # 7.1 będziesz mógł wykonać właściwą asynchronizację Main . Odpowiednie podpisy dla Mainmetody zostały rozszerzone na:

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

Na przykład możesz robić:

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

W czasie kompilacji metoda punktu wejścia asynchronicznego zostanie przetłumaczona na wywołanie GetAwaitor().GetResult().

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

EDYTOWAĆ:

Aby włączyć funkcje języka C # 7.1, należy kliknąć projekt prawym przyciskiem myszy i kliknąć „Właściwości”, a następnie przejść do zakładki „Kompilacja”. Tam kliknij przycisk zaawansowany u dołu:

wprowadź opis zdjęcia tutaj

Z menu rozwijanego wersji językowej wybierz „7.1” (lub dowolną wyższą wartość):

wprowadź opis zdjęcia tutaj

Domyślnie jest to „najnowsza wersja główna”, która w momencie pisania tego tekstu miałaby wartość C # 7.0, która nie obsługuje asynchronizacji głównej w aplikacjach konsolowych.


2
Oprogramowanie FWIW jest dostępne w programie Visual Studio 15.3 i nowszych wersjach, który jest obecnie dostępny jako wersja beta / wersja zapoznawcza
Mahmoud Al-Qudsi

Chwileczkę ... Korzystam z w pełni zaktualizowanej instalacji, a moją najnowszą opcją jest 7.1 ... jak dostałeś 7.2 już w maju?

Może odpowiedź była moja. Do czasu, gdy wydaje mi się, że wersja 7.2 (wersja zapoznawcza?) Mogła zostać wydana, ktoś dokonał edycji październikowej.
nawfal

1
Heads up - sprawdź, czy jest na wszystkich konfiguracjach, a nie tylko debuguj, gdy to zrobisz!
user230910

1
@ user230910 dzięki. Jeden z najdziwniejszych wyborów zespołu c #.
nawfal

74

Dodam ważną funkcję, którą przeoczyły wszystkie pozostałe odpowiedzi: anulowanie.

Jedną z najważniejszych rzeczy w TPL jest obsługa anulowania, a aplikacje konsolowe mają wbudowaną metodę anulowania (CTRL + C). Bardzo łatwo jest je ze sobą połączyć. Oto jak ustrukturyzowałem wszystkie moje aplikacje asynchronicznej konsoli:

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

Czy token anulowania należy przekazać Wait()również do?
Siewers

5
Nie, ponieważ chcesz, aby kod asynchroniczny mógł z łatwością obsługiwać anulowanie. Jeśli przekażesz go do Wait(), nie będzie czekać na zakończenie kodu asynchronicznego - przestanie czekać i natychmiast zakończy proces.
Cory Nelson

Jesteś pewien? Właśnie go wypróbowałem i wygląda na to, że prośba o anulowanie jest przetwarzana na najgłębszym poziomie, nawet jeśli Wait()metoda zostanie przekazana z tym samym tokenem. Próbuję powiedzieć, że to nie wydaje się mieć żadnej różnicy.
Siewers

4
Jestem pewien. Chcesz anulować operację, a nie czekać na zakończenie operacji. Chyba że nie obchodzi Cię zakończenie czyszczenia kodu lub jego wynik.
Cory Nelson

1
Tak, myślę, że rozumiem, to po prostu nie miało żadnego znaczenia w moim kodzie. Kolejną rzeczą, która mnie wyrzuciła z kursu, była uprzejma wskazówka ReSharpera na temat metody oczekiwania obsługującej anulowanie;) Być może warto dołączyć przykładowy haczyk, ponieważ spowoduje to
wyjątek

22

C # 7.1 (przy użyciu aktualizacji 3 vs 2017) wprowadza asynchroniczną wersję główną

Możesz pisać:

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

Aby uzyskać więcej informacji Seria C # 7, część 2: Asynchronizacja główna

Aktualizacja:

Może pojawić się błąd kompilacji:

Program nie zawiera statycznej metody „głównej” odpowiedniej dla punktu wejścia

Ten błąd wynika z faktu, że vs2017.3 jest skonfigurowany domyślnie jako c # 7.0, a nie c # 7.1.

Powinieneś jawnie zmodyfikować ustawienia swojego projektu, aby ustawić funkcje c # 7.1.

Możesz ustawić c # 7.1 na dwa sposoby:

Metoda 1: Korzystanie z okna ustawień projektu:

  • Otwórz ustawienia swojego projektu
  • Wybierz kartę Kompilacja
  • Kliknij przycisk Zaawansowane
  • Wybierz żądaną wersję Jak pokazano na poniższym rysunku:

wprowadź opis zdjęcia tutaj

Metoda 2: Ręcznie zmodyfikuj PropertyGroup .csproj

Dodaj tę właściwość:

    <LangVersion>7.1</LangVersion>

przykład:

    <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

Jeśli używasz C # 7.1 lub nowszej, przejdź do odpowiedzi nawfal i po prostu zmień typ zwracanej metody Main na Tasklub Task<int>. Jeśli nie jesteś:

Ostateczny kod wygląda następująco:

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
Wiele ładnych programów anuluje CancelKeyPress tylko za pierwszym razem, więc jeśli naciśniesz ^ C, gdy uzyskasz pełne wdzięczne zamknięcie, ale jeśli niecierpliwisz się, drugie ^ C zakończy się niemiło. Dzięki temu rozwiązaniu będziesz musiał ręcznie zabić program, jeśli nie zaakceptuje on CancellationToken, ponieważ e.Cancel = truejest bezwarunkowy.
binki

19

Nie potrzebowałem jeszcze tak dużo, ale kiedy użyłem aplikacji konsolowej do szybkich testów i wymagałem asynchronizacji, właśnie to rozwiązałem:

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

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

Ten przykład będzie działał niepoprawnie w przypadku, gdy trzeba zaplanować zadanie do bieżącego kontekstu, a następnie poczekać (na przykład możesz zapomnieć o dodaniu ConfigureAwait (false), więc metoda powrotu zostanie zaplanowana w głównym wątku, który jest w funkcji Czekaj) ). Ponieważ bieżący wątek jest w stanie oczekiwania, pojawi się zakleszczenie.
Manushin Igor,

6
Nieprawda, @ManushinIgor. Przynajmniej w tym trywialnym przykładzie nie ma SynchronizationContextpowiązania z głównym wątkiem. Więc nie zakleszczy się, ponieważ nawet bez ConfigureAwait(false), wszystkie kontynuacje zostaną wykonane w puli wątków.
Andrew Arnott,



4

Kiedy wprowadzono CTP C # 5, z pewnością można było oznaczyć Main jako async... chociaż generalnie nie był to dobry pomysł. Wydaje mi się, że zmieniło się to wraz z wydaniem VS 2013 i stało się błędem.

Jeśli nie rozpoczniesz żadnych innych wątków na pierwszym planie , program zakończy działanie po Mainzakończeniu, nawet jeśli rozpoczął pracę w tle.

Co naprawdę próbujesz zrobić? Zauważ, że twoja GetList()metoda naprawdę nie musi być teraz asynchroniczna - dodaje dodatkową warstwę bez prawdziwego powodu. Jest to logicznie równoważne (ale bardziej skomplikowane niż):

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

2
Jon, chcę uzyskać pozycje na liście asynchronicznie, więc dlaczego asynchronizacja nie jest odpowiednia dla tej metody GetList? Czy to dlatego, że muszę zebrać elementy na liście asynchronicznie, a nie samą listę? Kiedy próbuję oznaczyć metodę główną asynchronicznie, otrzymuję komunikat „nie zawiera statycznej metody głównej ...”
danielovich

@danielovich: Co DownloadTvChannels()powraca? Przypuszczalnie zwraca a Task<List<TvChannel>>nie? Jeśli nie, jest mało prawdopodobne, że będziesz w stanie na to poczekać. (Możliwe, biorąc pod uwagę wzór awaiter, ale mało prawdopodobne.) Jeśli chodzi o Mainmetody - to jeszcze musi być statyczna ... czy zastąpić ten staticmodyfikator z asyncmodyfikatora może?
Jon Skeet,

tak, zwraca zadanie <..> tak jak powiedziałeś. Bez względu na to, jak próbuję umieścić asynchronię w sygnaturze metody głównej, generuje błąd. Siedzę na bitach podglądu VS11!
danielovich

@danielovich: Nawet z nieważnym typem zwrotu? Po prostu public static async void Main() {}? Ale jeśli DownloadTvChannels()już zwraca a Task<List<TvChannel>>, prawdopodobnie jest już asynchroniczny - więc nie musisz dodawać kolejnej warstwy. Warto to dokładnie zrozumieć.
Jon Skeet,

1
@nawfal: Patrząc wstecz, myślę, że zmieniło się przed wydaniem VS2013. Nie jestem pewien, czy C # 7 to zmieni ...
Jon Skeet

4

Najnowsza wersja C # - C # 7.1 pozwala na tworzenie aplikacji konsoli asynchronicznej. Aby włączyć C # 7.1 w projekcie, musisz zaktualizować VS do co najmniej 15.3 i zmienić wersję C # na C# 7.1lub C# latest minor version. Aby to zrobić, przejdź do Właściwości projektu -> Kompilacja -> Zaawansowane -> Wersja językowa.

Następnie zadziała następujący kod:

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

3

W MSDN dokumentacja metody Task.Run (działanie) zawiera ten przykład, który pokazuje, jak uruchomić metodę asynchronicznie z 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

Zwróć uwagę na to oświadczenie, które następuje za przykładem:

Przykłady pokazują, że zadanie asynchroniczne wykonuje się w innym wątku niż główny wątek aplikacji.

Tak więc, jeśli chcesz zamiast zadanie uruchomić w głównym wątku aplikacji, patrz odpowiedź przez @StephenCleary .

Jeśli chodzi o wątek, w którym działa zadanie, zwróć również uwagę na komentarz Stephena do jego odpowiedzi:

Państwo może użyć prostego Waitlub Result, i nie ma nic złego. Należy jednak pamiętać, że istnieją dwie ważne różnice: 1) wszystkie asynckontynuacje są uruchamiane w puli wątków, a nie w głównym wątku, oraz 2) wszelkie wyjątki są zawinięte w plik AggregateException.

(Zobacz Obsługa wyjątków (Biblioteka zadań równoległych), aby dowiedzieć się, jak włączyć obsługę wyjątków w celu radzenia sobie z AggregateException.)


Na koniec w witrynie MSDN z dokumentacji dotyczącej metody Task.Delay (TimeSpan) ten przykład pokazuje, jak uruchomić zadanie asynchroniczne, które zwraca wartość:

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

Zauważ, że zamiast przekazywania delegatedo Task.Runmożesz przekazać funkcję lambda w następujący sposób:

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

1

Aby uniknąć zawieszania się, gdy wywołujesz funkcję gdzieś na stosie wywołań, który próbuje ponownie dołączyć do bieżącego wątku (który utknął w oczekiwaniu), musisz wykonać następujące czynności:

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

(obsada jest wymagana tylko w celu rozwiązania niejednoznaczności)


Dzięki; Task.Run nie powoduje impasu GetList (). Poczekaj, ta odpowiedź powinna mieć więcej głosów pozytywnych ...
Stefano d'Antonio

1

W moim przypadku miałem listę zadań, które chciałem uruchomić asynchronicznie z mojej głównej metody, używam tego w produkcji od dłuższego czasu i działa dobrze.

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 ...
}
Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.