Bagaimana cara menghindari melanggar prinsip KERING ketika Anda harus memiliki versi kode async dan sinkronisasi?

15

Saya sedang mengerjakan proyek yang perlu mendukung versi async dan sinkronisasi dari logika / metode yang sama. Jadi misalnya saya harus punya:

public class Foo
{
   public bool IsIt()
   {
      using (var conn = new SqlConnection(DB.ConnString))
      {
         return conn.Query<bool>("SELECT IsIt FROM SomeTable");
      }
   }

   public async Task<bool> IsItAsync()
   {
      using (var conn = new SqlConnection(DB.ConnString))
      {
         return await conn.QueryAsync<bool>("SELECT IsIt FROM SomeTable");
      }
   }
}

Logika async dan sinkronisasi untuk metode ini identik dalam segala hal kecuali bahwa yang satu adalah async dan yang lain tidak. Apakah ada cara yang sah untuk menghindari pelanggaran prinsip KERING dalam skenario semacam ini? Saya melihat bahwa orang mengatakan Anda bisa menggunakan GetAwaiter (). GetResult () pada metode async dan memintanya dari metode sinkronisasi Anda? Apakah utas itu aman di semua skenario? Apakah ada cara lain yang lebih baik untuk melakukan ini atau saya terpaksa menduplikasi logikanya?

Marko
sumber
1
Bisakah Anda mengatakan lebih banyak tentang apa yang Anda lakukan di sini? Secara khusus, bagaimana Anda mencapai asinkron dalam metode asinkron Anda? Apakah pekerjaan CPU dengan latensi tinggi terikat atau terikat IO?
Eric Lippert
"satu async dan yang lain tidak" apakah perbedaan dalam kode metode aktual atau hanya antarmuka? (Jelas hanya untuk antarmuka yang Anda bisa return Task.FromResult(IsIt());)
Alexei Levenkov
Juga, mungkin versi sinkron dan asinkron keduanya latensi tinggi. Dalam keadaan apa penelepon akan menggunakan versi sinkron, dan bagaimana penelepon itu akan mengurangi fakta bahwa callee dapat mengambil milyaran detik? Apakah penelepon versi sinkron tidak peduli bahwa mereka menggantung UI? Apakah tidak ada UI? Ceritakan lebih banyak tentang skenario ini kepada kami.
Eric Lippert
@ EricLippert Saya menambahkan contoh boneka khusus hanya untuk memberi Anda rasa bagaimana 2 basis kode identik selain fakta bahwa satu adalah async. Anda dapat dengan mudah membayangkan skenario di mana metode ini jauh lebih kompleks dan garis dan garis kode harus diduplikasi.
Marko
@AlexeiLevenkov Perbedaannya memang dalam kode, saya menambahkan beberapa kode dummy untuk demo itu.
Marko

Jawaban:

15

Anda mengajukan beberapa pertanyaan dalam pertanyaan Anda. Saya akan memecahnya sedikit berbeda dari yang Anda lakukan. Tapi pertama izinkan saya langsung menjawab pertanyaan.

Kita semua menginginkan kamera yang ringan, berkualitas tinggi, dan murah, tetapi seperti kata pepatah, Anda hanya bisa mendapatkan paling banyak dua dari tiga. Anda berada dalam situasi yang sama di sini. Anda menginginkan solusi yang efisien, aman, dan berbagi kode antara jalur sinkron dan asinkron. Anda hanya akan mendapatkan dua dari itu.

Biarkan saya menjelaskan mengapa itu. Kami akan mulai dengan pertanyaan ini:


Saya melihat bahwa orang mengatakan Anda dapat menggunakan GetAwaiter().GetResult()metode async dan memintanya dari metode sinkronisasi Anda? Apakah utas itu aman di semua skenario?

Inti dari pertanyaan ini adalah "dapatkah saya membagikan jalur sinkron dan asinkron dengan membuat jalur sinkron hanya menunggu secara sinkron pada versi asinkron?"

Biarkan saya menjadi sangat jelas tentang hal ini karena ini penting:

ANDA HARUS SEGERA BERHENTI MENGAMBIL NASIHAT DARI MEREKA ORANG .

Itu saran yang sangat buruk. Sangat berbahaya untuk secara sinkron mengambil hasil dari tugas asinkron kecuali Anda memiliki bukti bahwa tugas telah selesai secara normal atau tidak normal .

Alasan mengapa ini saran yang sangat buruk adalah, yah, pertimbangkan skenario ini. Anda ingin memotong rumput, tetapi bilah pemotong rumput Anda rusak. Anda memutuskan untuk mengikuti alur kerja ini:

  • Pesan bilah baru dari situs web. Ini adalah operasi latensi tinggi, asinkron.
  • Sinkron menunggu - yaitu, tidur sampai Anda memiliki pisau di tangan .
  • Periksa kotak surat secara berkala untuk melihat apakah blade telah tiba.
  • Hapus bilah dari kotak. Sekarang Anda memilikinya di tangan.
  • Pasang pisau di mesin pemotong rumput.
  • Memotong rumput.

Apa yang terjadi? Anda tidur selamanya karena operasi memeriksa surat sekarang terjaga keamanannya pada sesuatu yang terjadi setelah surat tiba .

Sangat mudah untuk masuk ke situasi ini ketika Anda secara serentak menunggu tugas yang sewenang-wenang. Tugas itu mungkin sudah dijadwalkan di masa depan utas yang sekarang menunggu , dan sekarang masa depan itu tidak akan pernah tiba karena Anda sedang menunggu untuk itu.

Jika Anda melakukan menunggu asinkron maka semuanya baik-baik saja! Anda memeriksa surat secara berkala, dan saat Anda menunggu, Anda membuat sandwich atau melakukan pajak atau apa pun; Anda terus menyelesaikan pekerjaan sambil menunggu.

Jangan menunggu secara sinkron. Jika tugas selesai, itu tidak perlu . Jika tugas tidak dilakukan tetapi dijadwalkan untuk menjalankan utas saat ini, itu tidak efisien karena utas saat ini dapat melayani pekerjaan lain alih-alih menunggu. Jika tugas tidak selesai dan jadwal berjalan pada utas saat ini, itu tergantung menunggu secara sinkron. Tidak ada alasan yang baik untuk menunggu secara sinkron, lagi, kecuali jika Anda sudah tahu bahwa tugas sudah selesai .

Untuk membaca lebih lanjut tentang topik ini, lihat

https://blog.stephencleary.com/2012/07/dont-block-on-async-code.html

Stephen menjelaskan skenario dunia nyata jauh lebih baik daripada yang saya bisa.


Sekarang mari kita pertimbangkan "arah lain". Bisakah kita membagikan kode dengan membuat versi asinkron hanya melakukan versi sinkron pada utas pekerja?

Itu mungkin dan memang mungkin ide yang buruk, karena alasan berikut.

  • Itu tidak efisien jika operasi sinkron adalah pekerjaan IO latensi tinggi. Ini pada dasarnya mempekerjakan seorang pekerja dan membuat pekerja itu tidur sampai tugas selesai. Utas sangat mahal . Mereka mengkonsumsi sejuta byte address space minimum secara default, mereka butuh waktu, mereka mengambil sumber daya sistem operasi; Anda tidak ingin membakar utas melakukan pekerjaan yang tidak berguna.

  • Operasi sinkron mungkin tidak ditulis agar aman utas.

  • Ini adalah teknik yang lebih masuk akal jika pekerjaan dengan latensi tinggi terikat prosesor, tetapi jika demikian maka Anda mungkin tidak ingin menyerahkannya kepada pekerja. Anda mungkin ingin menggunakan pustaka tugas paralel untuk memparalelkannya ke CPU sebanyak mungkin, Anda mungkin ingin logika pembatalan, dan Anda tidak bisa begitu saja membuat versi sinkron melakukan semua itu, karena dengan demikian itu akan menjadi versi asinkron .

Bacaan lebih lanjut; sekali lagi, Stephen menjelaskannya dengan sangat jelas:

Mengapa tidak menggunakan Task.Run:

https://blog.stephencleary.com/2013/11/taskrun-etiquette-examples-using.html

Lebih banyak skenario "lakukan dan jangan" untuk Task.Run:

https://blog.stephencleary.com/2013/11/taskrun-etiquette-examples-dont-use.html


Lalu, apa yang tersisa dari kita? Kedua teknik untuk berbagi kode menyebabkan deadlock atau inefisiensi besar. Kesimpulan yang kami capai adalah bahwa Anda harus membuat pilihan. Apakah Anda ingin program yang efisien dan benar serta menyenangkan penelepon, atau Anda ingin menyimpan beberapa penekanan tombol yang diperlukan dengan menduplikasi sejumlah kecil kode antara jalur sinkron dan asinkron? Anda tidak mendapatkan keduanya, saya khawatir.

Eric Lippert
sumber
Bisakah Anda jelaskan mengapa kembali Task.Run(() => SynchronousMethod())dalam versi async bukan yang diinginkan OP?
Guillaume Sasdy
2
@GuillaumeSasdy: Poster asli telah menjawab pertanyaan tentang apa pekerjaannya: terikat IO. Sangat boros untuk menjalankan tugas yang terikat IO pada utas pekerja!
Eric Lippert
Baiklah saya mengerti. Saya pikir Anda benar, dan juga jawaban @StriplingWarrior menjelaskan lebih lanjut.
Guillaume Sasdy
2
@ Marko hampir semua solusi yang memblokir thread pada akhirnya akan (di bawah beban tinggi) mengkonsumsi semua threadpool threads (yang biasanya digunakan untuk menyelesaikan operasi async). Akibatnya semua utas akan menunggu dan tidak akan ada yang memungkinkan operasi untuk menjalankan "operasi async selesai" bagian dari kode. Sebagian besar penyelesaian masalah baik-baik saja dalam skenario beban rendah (karena ada banyak thread, bahkan 2-3 diblokir sebagai bagian dari setiap operasi tunggal) ... Dan jika Anda dapat menjamin bahwa penyelesaian operasi async berjalan pada thread OS baru (bukan thread pool) itu bahkan dapat bekerja untuk semua
kasing
2
Terkadang tidak ada cara lain selain "cukup lakukan versi sinkron pada utas pekerja". Misalnya, begitulah cara Dns.GetHostEntryAsyncditerapkan di .NET, dan FileStream.ReadAsyncuntuk jenis file tertentu. OS tidak menyediakan antarmuka async, jadi runtime harus memalsukannya (dan itu bukan bahasa yang spesifik — katakanlah, runtime Erlang menjalankan seluruh pohon proses pekerja , dengan beberapa utas di dalamnya, untuk menyediakan disk I / O dan nama non-blocking resolusi).
Joker_vD
6

Sulit untuk memberikan jawaban satu ukuran untuk semua untuk yang satu ini. Sayangnya tidak ada cara sederhana dan sempurna untuk menggunakan kembali antara kode asinkron dan sinkron. Tetapi di sini ada beberapa prinsip untuk dipertimbangkan:

  1. Kode Asinkron dan Sinkron seringkali berbeda secara fundamental. Kode asinkron biasanya harus menyertakan token pembatalan, misalnya. Dan seringkali berakhir dengan memanggil metode yang berbeda (seperti contoh Anda memanggil Query()satu dan QueryAsync()yang lain), atau mengatur koneksi dengan pengaturan yang berbeda. Jadi, walaupun secara struktural mirip, sering kali ada cukup banyak perbedaan perilaku sehingga pantas diperlakukan sebagai kode terpisah dengan persyaratan berbeda. Perhatikan perbedaan antara implementasi metode Async dan Sinkronisasi di kelas File, misalnya: tidak ada upaya yang dilakukan untuk membuatnya menggunakan kode yang sama
  2. Jika Anda memberikan tanda tangan metode Asynchronous demi mengimplementasikan antarmuka, tetapi Anda kebetulan memiliki implementasi yang sinkron (yaitu tidak ada yang secara inheren async tentang apa yang dilakukan metode Anda), Anda dapat kembali Task.FromResult(...).
  3. Setiap potongan sinkron logika yang merupakan sama antara dua metode dapat diekstraksi dengan metode pembantu terpisah dan leveraged di kedua metode.

Semoga berhasil.

StriplingWarrior
sumber
-2

Mudah; memiliki panggilan sinkron satu async. Bahkan ada metode praktis Task<T>untuk melakukan hal itu:

public class Foo
{
   public bool IsIt()
   {
      var task = IsItAsync(); //no await here; we want the Task

      //Some tasks end up scheduled to run before you get them;
      //don't try to run them a second time
      if((int)task.Status > (int)TaskStatus.Created)
          //this call will block the current thread,
          //and unlike Run()/Wait() will prefer the current 
          //thread's TaskScheduler instead of a new thread.
          task.RunSynchronously(); 

      //if IsItAsync() can throw exceptions,
      //you still need a Wait() call to bring those back from the Task
      try{ 
          task.Wait();
          return task.Result;
      }
      catch(Exception ex) 
      { 
          //Handle IsItAsync() exceptions here;
          //remember to return something if you don't rethrow              
      }
   }

   public async Task<bool> IsItAsync()
   {
      // Some async logic
   }
}
KeithS
sumber
1
Lihat jawaban saya mengapa ini adalah praktik terburuk yang tidak boleh Anda lakukan.
Eric Lippert
1
Jawaban Anda berkaitan dengan GetAwaiter (). GetResult (). Saya menghindari itu. Kenyataannya adalah bahwa kadang-kadang Anda harus membuat metode berjalan secara sinkron untuk menggunakannya dalam tumpukan panggilan yang tidak dapat dibuat async. Jika menunggu sinkronisasi tidak boleh dilakukan, maka sebagai (mantan) anggota tim C #, tolong beri tahu saya mengapa Anda tidak menempatkan satu tetapi dua cara untuk secara sinkron menunggu tugas yang tidak sinkron ke dalam TPL.
KeithS
Orang yang mengajukan pertanyaan itu adalah Stephen Toub, bukan saya. Saya hanya bekerja di sisi bahasa.
Eric Lippert