Tidak dapat menentukan pengubah 'async' pada metode 'Utama' pada aplikasi konsol

445

Saya baru mengenal pemrograman asinkron dengan asyncpengubah. Saya mencoba mencari cara untuk memastikan bahwa Mainmetode aplikasi konsol saya benar-benar berjalan secara tidak sinkron.

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

Saya tahu ini tidak berjalan secara tidak sinkron dari "atas." Karena tidak mungkin untuk menentukan asyncpengubah pada Mainmetode, bagaimana saya bisa menjalankan kode secara tidak mainsinkron?

danielovich
sumber
23
Ini bukan lagi kasus di C # 7.1. Metode utama dapat berupa async
Vasily Sliounaiev
2
Berikut pengumuman posting blog C # 7.1 . Lihat bagian berjudul Async Main .
styfle

Jawaban:

382

Seperti yang Anda temukan, di VS11 kompiler akan melarang async Mainmetode. Ini diizinkan (tapi tidak pernah disarankan) di VS2010 dengan CTP Async.

Saya memiliki posting blog baru-baru ini tentang program konsol async / await dan asynchronous pada khususnya. Berikut beberapa informasi latar belakang dari pos pengantar:

Jika "menunggu" melihat bahwa yang ditunggu belum selesai, maka ia bertindak secara serempak. Ini memberitahu yang ditunggu untuk menjalankan sisa metode ketika selesai, dan kemudian kembali dari metode async. Menunggu juga akan menangkap konteks saat ini ketika melewati sisa metode ke menunggu.

Kemudian, ketika selesai menunggu, itu akan mengeksekusi sisa metode async (dalam konteks yang ditangkap).

Inilah mengapa ini menjadi masalah dalam program Konsol dengan async Main:

Ingat dari pos intro kami bahwa metode async akan kembali ke pemanggilnya sebelum selesai. Ini berfungsi dengan baik di aplikasi UI (metode ini hanya kembali ke loop peristiwa UI) dan aplikasi ASP.NET (metode ini kembali dari utas tetapi menjaga permintaan tetap hidup). Itu tidak bekerja dengan baik untuk program-program Konsol: Main kembali ke OS - jadi program Anda keluar.

Salah satu solusinya adalah menyediakan konteks Anda sendiri - "loop utama" untuk program konsol Anda yang kompatibel dengan async.

Jika Anda memiliki mesin dengan CTP Async, Anda dapat menggunakan GeneralThreadAffineContextdari My Documents \ Microsoft Visual Studio Async CTP \ Sampel (C # Testing) Unit Testing \ AsyncTestUtilities . Atau, Anda dapat menggunakan AsyncContextdari paket NuGet Nito.AsyncEx saya .

Berikut ini contoh menggunakan AsyncContext; GeneralThreadAffineContextmemiliki penggunaan yang hampir identik:

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

Atau, Anda bisa memblokir utas Konsol utama sampai pekerjaan asinkron Anda selesai:

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

Perhatikan penggunaan GetAwaiter().GetResult(); ini menghindari AggregateExceptionpembungkus yang terjadi jika Anda menggunakan Wait()atau Result.

Pembaruan, 2017-11-30: Pada Visual Studio 2017 Pembaruan 3 (15,3), bahasa sekarang mendukung async Main- selama kembali Taskatau Task<T>. Jadi sekarang Anda dapat melakukan ini:

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

Semantik tampaknya sama dengan GetAwaiter().GetResult()gaya memblokir utas. Namun, belum ada spesifikasi bahasa untuk C # 7.1, jadi ini hanya asumsi.

Stephen Cleary
sumber
30
Anda bisa menggunakan yang sederhana Waitatau Result, dan tidak ada yang salah dengan itu. Tetapi perlu diketahui bahwa ada dua perbedaan penting: 1) semua asynckelanjutan berjalan di kolam utas daripada utas utama, dan 2) setiap pengecualian dibungkus dalam AggregateException.
Stephen Cleary
2
Sedang mengalami masalah nyata mencari tahu sampai saat ini (dan posting blog Anda). Sejauh ini, ini adalah metode termudah untuk menyelesaikan masalah ini, dan Anda dapat menginstal paket di konsol nuget hanya dengan "install-package Nito.Asyncex" dan Anda selesai.
ConstantineK
1
@StephenCleary: Terima kasih atas respon cepat Stephen. Saya tidak mengerti mengapa ada orang yang tidak ingin debugger rusak ketika pengecualian dilemparkan. Jika saya men-debug dan menjalankan pengecualian referensi nol, langsung menuju baris kode yang menyinggung tampaknya lebih disukai. VS bekerja seperti itu "di luar kotak" untuk kode sinkron, tetapi tidak untuk async / menunggu.
Greg
6
C # 7.1 memiliki async main sekarang, mungkin layak ditambahkan ke jawaban Anda yang bagus, @StephenCleary github.com/dotnet/csharplang/blob/master/proposals/csharp-7.1/…
Mafii
3
Jika Anda menggunakan versi C # 7.1 di VS 2017, saya perlu memastikan proyek ini dikonfigurasi untuk menggunakan versi bahasa terbaru dengan menambahkan <LangVersion>latest</LangVersion>ke dalam file csproj, seperti yang ditunjukkan di sini .
Liam
359

Anda dapat menyelesaikan ini dengan konstruksi sederhana ini:

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

Itu akan menempatkan semua yang Anda lakukan di ThreadPool di tempat yang Anda inginkan (jadi Tugas lain yang Anda mulai / tunggu jangan mencoba untuk bergabung kembali dengan Thread yang seharusnya tidak mereka lakukan), dan tunggu sampai semuanya selesai sebelum menutup aplikasi Konsol. Tidak perlu untuk loop khusus atau lib luar.

Sunting: Memasukkan solusi Andrew untuk Pengecualian yang tidak tertangkap.

Chris Moschini
sumber
3
Pendekatan ini sangat jelas tetapi cenderung membungkus pengecualian jadi saya mencari cara yang lebih baik sekarang.
abatishchev
2
@abatishchev Anda harus menggunakan coba / tangkap dalam kode Anda, setidaknya di dalam Task.Run jika tidak lebih rinci, tidak membiarkan pengecualian melayang hingga Tugas. Anda akan menghindari masalah penutup dengan menempatkan mencoba / menangkap hal-hal yang dapat gagal.
Chris Moschini
54
Jika Anda mengganti Wait()dengan GetAwaiter().GetResult()Anda akan menghindari AggregateExceptionpembungkus ketika hal-hal melempar.
Andrew Arnott
7
Ini adalah bagaimana async maindiperkenalkan di C # 7.1, pada penulisan ini.
user9993
@ user9993 Menurut proposal ini , itu tidak sepenuhnya benar.
Sinjai
90

Anda dapat melakukan ini tanpa perlu perpustakaan eksternal juga dengan melakukan hal berikut:

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
sumber
7
Ingatlah bahwa getListTask.Resultitu juga merupakan panggilan pemblokiran sehingga kode di atas dapat ditulis tanpa Task.WaitAll(getListTask).
do0g
27
Juga, jika GetListmelempar, Anda harus menangkap AggregateExceptiondan menginterogasi pengecualiannya untuk menentukan pengecualian yang sebenarnya dilemparkan. Anda dapat, bagaimanapun, panggilan GetAwaiter()untuk mendapatkan TaskAwaiteruntuk itu Task, dan memanggil GetResult()itu, yaitu var list = getListTask.GetAwaiter().GetResult();. Saat mendapatkan hasil dari TaskAwaiter(juga panggilan pemblokiran) pengecualian apa pun yang dilemparkan tidak akan dibungkus dengan AggregateException.
do0g
1
.GetAwaiter (). GetResult adalah jawaban yang saya butuhkan. Itu bekerja dengan sempurna untuk apa yang saya coba lakukan. Saya mungkin akan menggunakan ini di tempat lain juga.
Deathstalker
78

Dalam C # 7.1 Anda akan dapat melakukan Main async yang tepat . Tanda tangan yang sesuai untuk Mainmetode telah diperluas ke:

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

Misalnya Anda bisa melakukan:

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

Pada waktu kompilasi, metode titik masuk async akan diterjemahkan ke panggilan GetAwaitor().GetResult() .

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

EDIT:

Untuk mengaktifkan fitur bahasa C # 7.1, Anda perlu klik kanan pada proyek dan klik "Properties" lalu pergi ke tab "Build". Di sana, klik tombol lanjutan di bagian bawah:

masukkan deskripsi gambar di sini

Dari menu tarik-turun versi bahasa, pilih "7.1" (atau nilai yang lebih tinggi):

masukkan deskripsi gambar di sini

Standarnya adalah "versi utama terbaru" yang akan mengevaluasi (pada saat penulisan ini) ke C # 7.0, yang tidak mendukung async utama di aplikasi konsol.

nawfal
sumber
2
FWIW ini tersedia dalam Visual Studio 15.3 dan lebih tinggi, yang saat ini tersedia sebagai rilis beta / pratinjau dari sini: visualstudio.com/vs/preview
Mahmoud Al-Qudsi
Tunggu sebentar ... Saya sedang menjalankan instalasi yang sepenuhnya diperbarui dan opsi terbaru saya adalah 7.1 ... bagaimana Anda mendapatkan 7.2 di bulan Mei?
Jawaban Mei adalah milikku. Sunting Oktober oleh orang lain pada saat saya pikir 7.2 (pratinjau?) Mungkin telah dirilis.
nawfal
1
Kepala - periksa apakah ada di semua konfigurasi, bukan hanya debug ketika Anda melakukan ini!
user230910
1
@ user230910 terima kasih. Salah satu pilihan paling aneh oleh tim c #.
nawfal
74

Saya akan menambahkan fitur penting yang diabaikan semua jawaban lainnya: pembatalan.

Salah satu hal besar dalam TPL adalah dukungan pembatalan, dan aplikasi konsol memiliki metode pembatalan bawaan (CTRL + C). Sangat mudah untuk mengikat mereka bersama. Ini adalah bagaimana saya menyusun semua aplikasi konsol async saya:

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
sumber
Haruskah token pembatalan diteruskan ke Wait()juga?
Siewers
5
Tidak, karena Anda ingin kode async dapat menangani pembatalan dengan anggun. Jika Anda meneruskannya ke Wait(), kode async tidak akan menunggu sampai selesai - itu akan berhenti menunggu dan segera mengakhiri proses.
Cory Nelson
Apa kamu yakin akan hal itu? Saya baru saja mencobanya, dan sepertinya permintaan pembatalan sedang diproses pada tingkat terdalam, bahkan ketika Wait()metode ini dilewati token yang sama. Yang ingin saya katakan adalah, sepertinya tidak ada bedanya.
Siewers
4
Saya yakin. Anda ingin membatalkan op itu sendiri, bukan menunggu op selesai. Kecuali Anda tidak peduli dengan penyelesaian kode pembersihan atau hasilnya.
Cory Nelson
1
Ya, saya pikir saya mengerti, sepertinya tidak ada bedanya dalam kode saya. Hal lain yang membuat saya keluar tentu saja adalah petunjuk ReSharper yang sopan tentang metode menunggu yang mendukung pembatalan;) Anda mungkin ingin menyertakan try catch dalam contoh ini, karena akan melempar OperationCancelledException, yang saya tidak tahu pada awalnya
Siewers
22

C # 7.1 (menggunakan vs 2017 pembaruan 3) memperkenalkan async main

Kamu bisa menulis:

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

Untuk detail lebih lanjut C # 7 Series, Bagian 2: Async Main

Memperbarui:

Anda mungkin mendapatkan kesalahan kompilasi:

Program tidak mengandung metode 'Utama' statis yang cocok untuk titik masuk

Kesalahan ini disebabkan bahwa vs2017.3 dikonfigurasi secara default sebagai c # 7.0 bukan c # 7.1.

Anda harus secara eksplisit mengubah pengaturan proyek Anda untuk menetapkan fitur c # 7.1.

Anda dapat mengatur c # 7.1 dengan dua metode:

Metode 1: Menggunakan jendela pengaturan proyek:

  • Buka pengaturan proyek Anda
  • Pilih tab Bangun
  • Klik tombol Lanjut
  • Pilih versi yang Anda inginkan Seperti yang ditunjukkan pada gambar berikut:

masukkan deskripsi gambar di sini

Metode2: Ubah PropertyGroup dari .csproj secara manual

Tambahkan properti ini:

    <LangVersion>7.1</LangVersion>

contoh:

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

Jika Anda menggunakan C # 7.1 atau lebih baru, pergilah dengan jawaban nawfal dan cukup ubah jenis pengembalian metode Utama Anda ke Taskatau Task<int>. Jika tidak:

  • Miliki async Task MainAsync seperti kata Johan .
  • Sebut .GetAwaiter().GetResult()untuk menangkap pengecualian yang mendasarinya seperti kata do0g .
  • Mendukung pembatalan seperti kata Cory .
  • Yang kedua CTRL+Charus segera menghentikan proses. (Terima kasih binki !)
  • Tangani OperationCancelledException- kembalikan kode kesalahan yang sesuai.

Kode akhir terlihat seperti:

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
sumber
1
Banyak program bagus akan membatalkan CancelKeyPress hanya pertama kali sehingga jika Anda menekan ^ C setelah Anda mendapatkan shutdown anggun tetapi jika Anda tidak sabar yang kedua ^ C berakhir dengan tidak berterima. Dengan solusi ini, Anda harus mematikan program secara manual jika gagal untuk menghormati PembatalanToken karena e.Cancel = truetidak bersyarat.
binki
19

Belum terlalu membutuhkan ini, tetapi ketika saya telah menggunakan aplikasi konsol untuk tes cepat dan diperlukan async saya baru saja menyelesaikannya seperti ini:

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

    static async Task MainAsync(string[] args)
    {
        // Code here
    }
}
Johan Falk
sumber
Contoh ini akan berfungsi salah jika Anda harus menjadwalkan tugas ke konteks saat ini dan kemudian menunggu (misalnya, Anda dapat lupa menambahkan ConfigureAwait (false), jadi metode pengembalian akan dijadwalkan ke utas utama, yang ada dalam fungsi Tunggu ). Karena utas saat ini dalam status menunggu, Anda akan menerima jalan buntu.
Manushin Igor
6
Tidak benar, @ManushinIgor. Setidaknya dalam contoh sepele ini, tidak ada yang SynchronizationContextterkait dengan utas utama. Jadi itu tidak akan menemui jalan buntu karena bahkan tanpa ConfigureAwait(false), semua kelanjutan akan dijalankan di threadpool.
Andrew Arnott
4

Di Utama, coba ubah panggilan ke GetList ke:

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

Ketika C # 5 CTP diperkenalkan, Anda tentu bisa menandai Main denganasync ... meskipun itu umumnya bukan ide yang baik untuk melakukannya. Saya percaya ini diubah oleh rilis VS 2013 menjadi kesalahan.

Kecuali jika Anda telah memulai utas latar depan lainnya , program Anda akan keluar saat Mainselesai, bahkan jika itu memulai beberapa pekerjaan latar belakang.

Apa yang sebenarnya ingin Anda lakukan? Perhatikan bahwa GetList()metode Anda benar-benar tidak perlu async saat ini - itu menambahkan lapisan tambahan tanpa alasan nyata. Secara logis setara dengan (tetapi lebih rumit dari):

public Task<List<TvChannel>> GetList()
{
    return new GetPrograms().DownloadTvChannels();
}
Jon Skeet
sumber
2
Jon, saya ingin mendapatkan item dalam daftar secara tidak sinkron, jadi mengapa async tidak sesuai pada metode GetList? Apakah karena saya perlu mengumpulkan item dalam daftar async 'dan bukan daftar itu sendiri? Ketika saya mencoba menandai metode Utama dengan async saya mendapatkan "tidak mengandung metode Utama statis ..."
danielovich
@danielovich: Apa yang DownloadTvChannels()dikembalikan? Mungkin mengembalikannya Task<List<TvChannel>>bukan? Jika tidak, kecil kemungkinan Anda akan bisa menunggu. (Kemungkinan, mengingat pola awaiter, tapi tidak mungkin.) Adapun Mainmetode - masih perlu statis ... kau ganti dengan staticmodifier dengan asyncmodifier mungkin?
Jon Skeet
ya, ia mengembalikan Tugas <..> seperti yang Anda katakan. Tidak peduli bagaimana saya mencoba memasukkan async pada tanda tangan metode Utama, ia melempar kesalahan. Saya duduk di bit preview VS11!
danielovich
@danielovich: Bahkan dengan tipe pengembalian kosong? Adil public static async void Main() {}? Tetapi jika DownloadTvChannels()sudah mengembalikan Task<List<TvChannel>>, mungkin sudah asinkron - jadi Anda tidak perlu menambahkan lapisan lain. Perlu dipahami dengan cermat.
Jon Skeet
1
@nawfal: Melihat ke belakang, saya pikir itu berubah sebelum VS2013 dirilis. Tidak yakin apakah C # 7 akan mengubah itu ...
Jon Skeet
4

Versi terbaru dari C # - C # 7.1 memungkinkan untuk membuat aplikasi konsol async. Untuk mengaktifkan C # 7.1 dalam proyek, Anda harus meningkatkan VS Anda ke setidaknya 15,3, dan mengubah versi C # ke C# 7.1atau C# latest minor version. Untuk melakukannya, buka Properti proyek -> Bangun -> Tingkat Lanjut -> Versi bahasa.

Setelah ini, kode berikut akan berfungsi:

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

Pada MSDN, dokumentasi untuk Metode Task.Run (Tindakan) memberikan contoh ini yang menunjukkan cara menjalankan metode secara asinkron dari 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

Perhatikan pernyataan ini yang mengikuti contoh:

Contoh menunjukkan bahwa tugas asinkron dijalankan pada utas berbeda dari utas aplikasi utama.

Jadi, jika Anda ingin tugas dijalankan di utas aplikasi utama, lihat jawabannya oleh @StephenCleary .

Dan mengenai utas tempat tugas itu berjalan, perhatikan juga komentar Stephen tentang jawabannya:

Anda bisa menggunakan yang sederhana Waitatau Result, dan tidak ada yang salah dengan itu. Tetapi perlu diketahui bahwa ada dua perbedaan penting: 1) semua asynckelanjutan berjalan di kolam utas daripada utas utama, dan 2) setiap pengecualian dibungkus dalam AggregateException.

(Lihat Penanganan Pengecualian (Perpustakaan Tugas Paralel) untuk cara menggabungkan penanganan pengecualian untuk menangani AggregateException.)


Akhirnya, di MSDN dari dokumentasi untuk Metode Task.Delay (TimeSpan) , contoh ini menunjukkan cara menjalankan tugas asinkron yang mengembalikan nilai:

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

Perhatikan bahwa alih-alih meneruskan delegateke Task.Run, Anda dapat meneruskan fungsi lambda seperti ini:

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

Untuk menghindari pembekuan ketika Anda memanggil suatu fungsi di suatu tempat di tumpukan panggilan yang mencoba untuk bergabung kembali dengan utas saat ini (yang terjebak dalam Tunggu), Anda perlu melakukan hal berikut:

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

(Para pemain hanya diminta untuk menyelesaikan ambiguitas)

Nathan Phillips
sumber
Terima kasih; Task.Run tidak menyebabkan kebuntuan dari GetList (). Tunggu, jawaban ini pasti memiliki lebih banyak upvotes ...
Stefano d'Antonio
1

Dalam kasus saya, saya memiliki daftar pekerjaan yang ingin saya jalankan di async dari metode utama saya, telah menggunakan ini dalam produksi selama beberapa waktu dan berfungsi dengan baik.

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
sumber