Haruskah saya khawatir tentang "Metode async ini tidak memiliki operator 'menunggu' dan akan berjalan secara sinkron”

93

Saya memiliki antarmuka yang mengekspos beberapa metode asinkron. Lebih khusus lagi, ia memiliki metode yang ditentukan yang mengembalikan Tugas atau Tugas <T>. Saya menggunakan kata kunci async / await.

Saya sedang dalam proses menerapkan antarmuka ini. Namun, dalam beberapa metode ini, implementasi ini tidak memiliki apa-apa untuk menunggu. Untuk alasan itu saya mendapatkan peringatan compiler "Metode async ini tidak memiliki operator 'await' dan akan berjalan serentak ..."

Saya mengerti mengapa saya mendapatkan kesalahan tetapi saya bertanya-tanya apakah saya harus melakukan sesuatu tentang mereka dalam konteks ini. Rasanya salah jika mengabaikan peringatan compiler.

Saya tahu saya bisa memperbaikinya dengan menunggu di Task.Run tetapi itu terasa salah untuk metode yang hanya melakukan beberapa operasi murah. Ini juga terdengar seperti itu akan menambah overhead yang tidak diperlukan ke eksekusi tetapi kemudian saya juga tidak yakin apakah itu sudah ada karena kata kunci async ada.

Haruskah saya mengabaikan peringatan atau adakah cara untuk mengatasi hal ini yang tidak saya lihat?

dannykay1710
sumber
2
Ini akan tergantung pada spesifikasinya. Apakah Anda benar-benar yakin ingin operasi ini dilakukan secara sinkron? Jika Anda ingin melakukannya secara sinkron, mengapa metode ini ditandai sebagai async?
Pelayanan
11
Hapus saja asynckata kunci tersebut. Anda masih bisa kembali Taskmenggunakan Task.FromResult.
Michael Liu
1
@BenVoigt Google penuh dengan informasi tentang itu, seandainya OP belum mengetahuinya.
Pelayanan
1
@BenVoigt Bukankah Michael Liu sudah memberikan petunjuk itu? Gunakan Task.FromResult.
1
@hvd: Itu telah diedit menjadi komentarnya nanti.
Ben Voigt

Jawaban:

144

Kata kunci async hanyalah sebuah detail implementasi dari sebuah metode; itu bukan bagian dari tanda tangan metode. Jika salah satu implementasi atau penggantian metode tertentu tidak memiliki apa-apa untuk menunggu, hilangkan saja kata kunci async dan kembalikan tugas yang sudah selesai menggunakan Task.FromResult <TResult> :

public Task<string> Foo()               //    public async Task<string> Foo()
{                                       //    {
    Baz();                              //        Baz();
    return Task.FromResult("Hello");    //        return "Hello";
}                                       //    }

Jika metode Anda mengembalikan Tugas dan bukan Tugas <TResult> , Anda dapat mengembalikan tugas yang sudah selesai dari semua jenis dan nilai. Task.FromResult(0)tampaknya menjadi pilihan populer:

public Task Bar()                       //    public async Task Bar()
{                                       //    {
    Baz();                              //        Baz();
    return Task.FromResult(0);          //
}                                       //    }

Atau, mulai .NET Framework 4.6, Anda dapat mengembalikan Task.CompletedTask :

public Task Bar()                       //    public async Task Bar()
{                                       //    {
    Baz();                              //        Baz();
    return Task.CompletedTask;          //
}                                       //    }
Michael Liu
sumber
Terima kasih Saya pikir sedikit yang saya lewatkan adalah konsep membuat Tugas yang telah selesai, daripada mengembalikan tugas sebenarnya yang seperti yang Anda katakan akan sama dengan memiliki kata kunci async. Tampak jelas sekarang tapi saya hanya tidak melihatnya!
dannykay1710
1
Tugas dapat dilakukan dengan anggota statis di sepanjang baris Task.Empty untuk tujuan ini. Niatnya akan menjadi sedikit lebih jelas dan menyakitkan bagi saya untuk memikirkan semua Tugas berbakti ini yang menghasilkan nol yang tidak pernah diperlukan.
Rupert Rawnsley
await Task.FromResult(0)? Bagaimana dengan await Task.Yield()?
Sushi271
1
@ Sushi271: Tidak, dalam asyncmetode non- , Anda kembali Task.FromResult(0) alih-alih menunggu.
Michael Liu
1
Sebenarnya TIDAK, async bukan hanya detail implementasi, ada banyak detail seputar yang harus diperhatikan :). Kita harus sadar, bagian mana yang berjalan secara sinkron, bagian mana yang tidak sinkron, apa konteks sinkronisasi saat ini dan hanya untuk catatan, Tugas selalu sedikit lebih cepat, karena tidak ada mesin negara di balik tirai :).
ipavlu
16

Sangat masuk akal bahwa beberapa operasi "asinkron" selesai secara serempak, namun tetap sesuai dengan model panggilan asinkron demi polimorfisme.

Contoh dunia nyata ini adalah dengan OS I / O API. Panggilan asinkron dan tumpang tindih pada beberapa perangkat selalu selesai sebaris (menulis ke pipa yang diimplementasikan menggunakan memori bersama, misalnya). Tapi mereka mengimplementasikan antarmuka yang sama seperti operasi multi-bagian yang berlanjut di latar belakang.

Ben Voigt
sumber
4

Michael Liu menjawab dengan baik pertanyaan Anda tentang bagaimana Anda dapat menghindari peringatan: dengan mengembalikan Task.FromResult.

Saya akan menjawab bagian "Haruskah saya khawatir tentang peringatan" dari pertanyaan Anda.

Jawabannya iya!

Alasannya adalah karena peringatan sering muncul saat Anda memanggil metode yang mengembalikan Taskdi dalam metode asinkron tanpa awaitoperator. Saya baru saja memperbaiki bug konkurensi yang terjadi karena saya menjalankan operasi di Entity Framework tanpa menunggu operasi sebelumnya.

Jika Anda dapat dengan cermat menulis kode Anda untuk menghindari peringatan compiler, maka ketika ada peringatan, itu akan menonjol seperti jempol yang sakit. Saya bisa menghindari beberapa jam debugging.

Sungai Vivian
sumber
5
Jawaban ini salah. Inilah alasannya: setidaknya ada satu awaitdi dalam metode di satu tempat (tidak akan ada CS1998) tetapi itu tidak berarti bahwa tidak akan ada panggilan lain dari metode asnyc yang tidak memiliki sinkronisasi (menggunakan awaitatau lainnya). Sekarang jika seseorang ingin tahu cara memastikan Anda tidak melewatkan sinkronisasi secara tidak sengaja, pastikan Anda tidak mengabaikan peringatan lain - CS4014. Saya bahkan akan merekomendasikan untuk mengancam yang satu itu sebagai kesalahan.
Victor Yarema
3

Mungkin sudah terlambat tapi mungkin investigasi berguna:

Ada tentang struktur dalam kode terkompilasi ( IL ):

 public static async Task<int> GetTestData()
    {
        return 12;
    }

menjadi di IL:

.method private hidebysig static class [mscorlib]System.Threading.Tasks.Task`1<int32> 
        GetTestData() cil managed
{
  .custom instance void [mscorlib]System.Runtime.CompilerServices.AsyncStateMachineAttribute::.ctor(class [mscorlib]System.Type) = ( 01 00 28 55 73 61 67 65 4C 69 62 72 61 72 79 2E   // ..(UsageLibrary.
                                                                                                                                     53 74 61 72 74 54 79 70 65 2B 3C 47 65 74 54 65   // StartType+<GetTe
                                                                                                                                     73 74 44 61 74 61 3E 64 5F 5F 31 00 00 )          // stData>d__1..
  .custom instance void [mscorlib]System.Diagnostics.DebuggerStepThroughAttribute::.ctor() = ( 01 00 00 00 ) 
  // Code size       52 (0x34)
  .maxstack  2
  .locals init ([0] class UsageLibrary.StartType/'<GetTestData>d__1' V_0,
           [1] valuetype [mscorlib]System.Runtime.CompilerServices.AsyncTaskMethodBuilder`1<int32> V_1)
  IL_0000:  newobj     instance void UsageLibrary.StartType/'<GetTestData>d__1'::.ctor()
  IL_0005:  stloc.0
  IL_0006:  ldloc.0
  IL_0007:  call       valuetype [mscorlib]System.Runtime.CompilerServices.AsyncTaskMethodBuilder`1<!0> valuetype [mscorlib]System.Runtime.CompilerServices.AsyncTaskMethodBuilder`1<int32>::Create()
  IL_000c:  stfld      valuetype [mscorlib]System.Runtime.CompilerServices.AsyncTaskMethodBuilder`1<int32> UsageLibrary.StartType/'<GetTestData>d__1'::'<>t__builder'
  IL_0011:  ldloc.0
  IL_0012:  ldc.i4.m1
  IL_0013:  stfld      int32 UsageLibrary.StartType/'<GetTestData>d__1'::'<>1__state'
  IL_0018:  ldloc.0
  IL_0019:  ldfld      valuetype [mscorlib]System.Runtime.CompilerServices.AsyncTaskMethodBuilder`1<int32> UsageLibrary.StartType/'<GetTestData>d__1'::'<>t__builder'
  IL_001e:  stloc.1
  IL_001f:  ldloca.s   V_1
  IL_0021:  ldloca.s   V_0
  IL_0023:  call       instance void valuetype [mscorlib]System.Runtime.CompilerServices.AsyncTaskMethodBuilder`1<int32>::Start<class UsageLibrary.StartType/'<GetTestData>d__1'>(!!0&)
  IL_0028:  ldloc.0
  IL_0029:  ldflda     valuetype [mscorlib]System.Runtime.CompilerServices.AsyncTaskMethodBuilder`1<int32> UsageLibrary.StartType/'<GetTestData>d__1'::'<>t__builder'
  IL_002e:  call       instance class [mscorlib]System.Threading.Tasks.Task`1<!0> valuetype [mscorlib]System.Runtime.CompilerServices.AsyncTaskMethodBuilder`1<int32>::get_Task()
  IL_0033:  ret
} // end of method StartType::GetTestData

Dan tanpa async dan metode tugas:

 public static int GetTestData()
        {
            return 12;
        }

menjadi :

.method private hidebysig static int32  GetTestData() cil managed
{
  // Code size       8 (0x8)
  .maxstack  1
  .locals init ([0] int32 V_0)
  IL_0000:  nop
  IL_0001:  ldc.i4.s   12
  IL_0003:  stloc.0
  IL_0004:  br.s       IL_0006
  IL_0006:  ldloc.0
  IL_0007:  ret
} // end of method StartType::GetTestData

Seperti yang Anda lihat perbedaan besar antara metode ini. Jika Anda tidak menggunakan metode await inside async dan tidak peduli tentang penggunaan metode async (misalnya panggilan API atau event handler), sebaiknya Anda mengubahnya menjadi metode sinkronisasi normal (ini menghemat kinerja aplikasi Anda).

Diperbarui:

Ada juga informasi tambahan dari microsoft docs https://docs.microsoft.com/en-us/dotnet/standard/async-in-depth :

metode async perlu memiliki kata kunci await di tubuhnya atau metode tersebut tidak akan pernah berhasil! Ini penting untuk diingat. Jika await tidak digunakan dalam tubuh metode async, kompilator C # akan menghasilkan peringatan, tetapi kode akan dikompilasi dan dijalankan seolah-olah itu adalah metode normal. Perhatikan bahwa ini juga akan sangat tidak efisien, karena mesin status yang dibuat oleh kompiler C # untuk metode asinkron tidak akan menyelesaikan apa pun.

Oleg Bondarenko
sumber
2
Selain itu, kesimpulan akhir Anda atas penggunaan of async/awaitsangat disederhanakan karena Anda mendasarkannya pada contoh tidak realistis dari operasi tunggal yang terikat CPU. TaskJika digunakan dengan benar memungkinkan peningkatan kinerja dan daya tanggap aplikasi karena tugas bersamaan (yaitu paralel) dan pengelolaan serta penggunaan utas yang lebih baik
MickyD
Itu hanya uji contoh yang disederhanakan seperti yang saya katakan di posting ini. Juga saya sebutkan tentang permintaan ke api dan event hendlers di mana mungkin menggunakan kedua versi metode (async dan reguler). Juga PO mengatakan tentang menggunakan metode async tanpa menunggu di dalam. Posting saya tentang itu tetapi tidak tentang penggunaan yang benar Tasks. Ini adalah cerita sedih bahwa Anda tidak membaca seluruh teks dari posting dan membuat kesimpulan dengan cepat.
Oleg Bondarenko
1
Ada perbedaan antara metode yang mengembalikan int(seperti dalam kasus Anda) dan yang mengembalikan Taskseperti yang dibahas oleh OP. Baca nya pos dan jawaban diterima lagi bukannya mengambil hal-hal pribadi. Jawaban Anda tidak membantu dalam kasus ini. Anda bahkan tidak perlu repot-repot untuk menunjukkan perbedaan antara metode yang ada awaitdi dalam atau tidak. Sekarang seandainya Anda melakukannya, itu akan sangat bagus untuk mendapat upvote
MickyD
Saya kira Anda benar-benar tidak mengerti perbedaan antara metode async dan metode biasa yang dipanggil dengan api atau event handler. Itu secara khusus disebutkan di posting saya. Maaf karena Anda melewatkannya lagi .
Oleg Bondarenko
1

Catatan tentang perilaku pengecualian saat kembali Task.FromResult

Berikut adalah demo kecil yang menunjukkan perbedaan dalam penanganan pengecualian antara metode yang ditandai dan tidak ditandai async.

public Task<string> GetToken1WithoutAsync() => throw new Exception("Ex1!");

// Warning: This async method lacks 'await' operators and will run synchronously. Consider ...
public async Task<string> GetToken2WithAsync() => throw new Exception("Ex2!");  

public string GetToken3Throws() => throw new Exception("Ex3!");
public async Task<string> GetToken3WithAsync() => await Task.Run(GetToken3Throws);

public async Task<string> GetToken4WithAsync() { throw new Exception("Ex4!"); return await Task.FromResult("X");} 


public static async Task Main(string[] args)
{
    var p = new Program();

    try { var task1 = p.GetToken1WithoutAsync(); } 
    catch( Exception ) { Console.WriteLine("Throws before await.");};

    var task2 = p.GetToken2WithAsync(); // Does not throw;
    try { var token2 = await task2; } 
    catch( Exception ) { Console.WriteLine("Throws on await.");};

    var task3 = p.GetToken3WithAsync(); // Does not throw;
    try { var token3 = await task3; } 
    catch( Exception ) { Console.WriteLine("Throws on await.");};

    var task4 = p.GetToken4WithAsync(); // Does not throw;
    try { var token4 = await task4; } 
    catch( Exception ) { Console.WriteLine("Throws on await.");};
}
// .NETCoreApp,Version=v3.0
Throws before await.
Throws on await.
Throws on await.
Throws on await.

(Posting silang dari jawaban saya untuk When async Task <T> diperlukan oleh antarmuka, bagaimana mendapatkan variabel kembali tanpa peringatan kompiler )

tymtam.dll
sumber