Jika async-waiting tidak membuat utas tambahan, lalu bagaimana cara membuat aplikasi responsif?

242

Berkali-kali, saya melihatnya mengatakan bahwa menggunakan async- awaittidak membuat utas tambahan. Itu tidak masuk akal karena satu-satunya cara agar komputer dapat melakukan lebih dari satu hal pada satu waktu adalah

  • Sebenarnya melakukan lebih dari 1 hal dalam satu waktu (mengeksekusi secara paralel, memanfaatkan beberapa prosesor)
  • Simulasi dengan menjadwalkan tugas dan beralih di antara mereka (lakukan sedikit A, sedikit B, sedikit A, dll.)

Jadi jika async- awaittidak satu pun dari itu, lalu bagaimana bisa membuat aplikasi responsif? Jika hanya ada 1 utas, maka memanggil metode apa pun berarti menunggu metode selesai sebelum melakukan hal lain, dan metode di dalam metode tersebut harus menunggu hasilnya sebelum melanjutkan, dan seterusnya.

Ms. Corlib
sumber
17
Tugas IO tidak terikat CPU dan karenanya tidak memerlukan utas. Titik utama async adalah untuk tidak memblokir utas selama tugas terikat IO.
juharr
24
@ jdweng: Tidak, tidak sama sekali. Bahkan jika itu membuat utas baru , itu sangat berbeda dari membuat proses baru.
Jon Skeet
8
Jika Anda memahami pemrograman asinkron berbasis callback, maka Anda mengerti bagaimana await/ asyncbekerja tanpa membuat utas.
user253751
6
Ini sebenarnya tidak membuat aplikasi lebih responsif, tetapi itu membuat Anda tidak bisa memblokir utas Anda, yang merupakan penyebab umum dari aplikasi yang tidak responsif.
Owen
6
@RubberDuck: Ya, mungkin menggunakan utas dari kumpulan utas untuk kelanjutan. Tapi itu tidak memulai utas dengan cara yang dibayangkan OP di sini - tidak seperti katanya "Ambil metode biasa ini, sekarang jalankan di utas terpisah - di sana, itu async." Jauh lebih halus dari itu.
Jon Skeet

Jawaban:

299

Sebenarnya, async / menunggu tidaklah ajaib. Topik lengkapnya cukup luas tetapi untuk jawaban yang cepat namun cukup lengkap untuk pertanyaan Anda, saya pikir kami bisa mengaturnya.

Mari kita atasi acara klik tombol sederhana di aplikasi Windows Forms:

public async void button1_Click(object sender, EventArgs e)
{
    Console.WriteLine("before awaiting");
    await GetSomethingAsync();
    Console.WriteLine("after awaiting");
}

Saya akan secara eksplisit tidak berbicara tentang apa pun GetSomethingAsyncyang dikembalikan untuk saat ini. Anggap saja ini adalah sesuatu yang akan selesai setelah, katakanlah, 2 detik.

Dalam dunia tradisional, non-asinkron, dunia, pengendali event klik tombol Anda akan terlihat seperti ini:

public void button1_Click(object sender, EventArgs e)
{
    Console.WriteLine("before waiting");
    DoSomethingThatTakes2Seconds();
    Console.WriteLine("after waiting");
}

Ketika Anda mengklik tombol dalam formulir, aplikasi akan muncul membeku sekitar 2 detik, sementara kami menunggu metode ini selesai. Apa yang terjadi adalah bahwa "pompa pesan", pada dasarnya sebuah loop, diblokir.

Lingkaran ini terus-menerus bertanya kepada windows "Apakah ada yang melakukan sesuatu, seperti menggerakkan mouse, mengklik sesuatu? Apakah saya perlu mengecat ulang sesuatu? Jika demikian, beri tahu saya!" dan kemudian memproses "sesuatu" itu. Loop ini mendapat pesan bahwa pengguna mengklik "button1" (atau jenis pesan yang setara dari Windows), dan akhirnya memanggil button1_Clickmetode kami di atas. Sampai metode ini kembali, loop ini sekarang macet menunggu. Ini membutuhkan waktu 2 detik dan selama ini, tidak ada pesan yang sedang diproses.

Sebagian besar hal yang berhubungan dengan windows dilakukan dengan menggunakan pesan, yang berarti bahwa jika loop pesan berhenti memompa pesan, bahkan hanya sesaat, itu dengan cepat dapat dilihat oleh pengguna. Misalnya, jika Anda memindahkan notepad atau program lain di atas program Anda sendiri, dan kemudian pergi lagi, sejumlah pesan cat dikirim ke program Anda yang menunjukkan wilayah jendela yang sekarang tiba-tiba menjadi terlihat lagi. Jika loop pesan yang memproses pesan-pesan ini sedang menunggu sesuatu, diblokir, maka tidak ada lukisan yang dilakukan.

Jadi, jika dalam contoh pertama, async/awaittidak membuat utas baru, bagaimana cara melakukannya?

Nah, yang terjadi adalah metode Anda dibagi menjadi dua. Ini adalah salah satu dari jenis topik yang luas sehingga saya tidak akan membahas terlalu banyak detail tetapi cukup untuk mengatakan metode ini dibagi menjadi dua hal ini:

  1. Semua kode mengarah ke await, termasuk panggilan keGetSomethingAsync
  2. Semua kode berikut await

Ilustrasi:

code... code... code... await X(); ... code... code... code...

Ulang:

code... code... code... var x = X(); await X; code... code... code...
^                                  ^          ^                     ^
+---- portion 1 -------------------+          +---- portion 2 ------+

Pada dasarnya metode dijalankan seperti ini:

  1. Itu mengeksekusi semuanya hingga await
  2. Itu memanggil GetSomethingAsyncmetode, yang melakukan hal itu, dan mengembalikan sesuatu yang akan menyelesaikan 2 detik di masa depan

    Sejauh ini kita masih di dalam panggilan asli ke button1_Click, terjadi di utas utama, dipanggil dari loop pesan. Jika pembuatan kode awaitmembutuhkan banyak waktu, UI akan tetap membeku. Dalam contoh kita, tidak banyak

  3. Apa awaitkata kunci, bersama dengan beberapa sihir kompiler pintar, apakah itu pada dasarnya sesuatu seperti "Oke, Anda tahu apa, saya hanya akan kembali dari pengendali event klik tombol di sini. Ketika Anda (seperti, hal yang kami sedang menunggu) menyiasati untuk menyelesaikan, beri tahu saya karena saya masih memiliki beberapa kode tersisa untuk dieksekusi ".

    Sebenarnya itu akan membiarkan kelas SynchronizationContext tahu bahwa itu dilakukan, yang, tergantung pada konteks sinkronisasi aktual yang sedang dimainkan saat ini, akan mengantri untuk dieksekusi. Kelas konteks yang digunakan dalam program Windows Forms akan mengantri menggunakan antrian yang dipompa oleh loop pesan.

  4. Jadi ia kembali ke loop pesan, yang sekarang bebas untuk terus memompa pesan, seperti memindahkan jendela, mengubah ukurannya, atau mengklik tombol lain.

    Untuk pengguna, UI sekarang responsif lagi, memproses klik tombol lainnya, mengubah ukuran dan yang paling penting, menggambar ulang , sehingga tampaknya tidak membeku.

  5. 2 detik kemudian, hal yang kita tunggu selesaikan dan apa yang terjadi sekarang adalah bahwa itu (well, konteks sinkronisasi) menempatkan pesan ke dalam antrian yang dilihat oleh loop pesan, mengatakan "Hei, saya punya beberapa kode lagi untuk Anda mengeksekusi ", dan kode ini adalah semua kode setelah menunggu.
  6. Ketika loop pesan sampai ke pesan itu, pada dasarnya ia akan "memasukkan kembali" metode yang ditinggalkannya, tepat setelah awaitdan terus menjalankan sisa metode. Perhatikan bahwa kode ini sekali lagi dipanggil dari loop pesan sehingga jika kode ini melakukan sesuatu yang panjang tanpa menggunakan async/awaitdengan benar, itu lagi akan memblokir loop pesan

Ada banyak bagian yang bergerak di bawah tenda di sini jadi di sini ada beberapa tautan ke informasi lebih lanjut, saya akan mengatakan "jika Anda membutuhkannya", tetapi topik ini cukup luas dan cukup penting untuk mengetahui beberapa bagian yang bergerak itu . Selalu Anda akan mengerti bahwa async / menunggu masih konsep yang bocor. Beberapa batasan dan masalah mendasar masih bocor ke kode di sekitarnya, dan jika tidak, Anda biasanya harus men-debug aplikasi yang rusak secara acak karena tampaknya tidak ada alasan yang bagus.


OK, jadi bagaimana jika GetSomethingAsyncmemutar utas yang akan selesai dalam 2 detik? Ya, maka jelas ada utas baru dalam permainan. Namun, utas ini bukan karena async-ness metode ini, melainkan karena pemrogram metode ini memilih utas untuk mengimplementasikan kode asinkron. Hampir semua I / O tidak sinkron tidak menggunakan utas, mereka menggunakan hal-hal yang berbeda. async/await sendiri tidak memunculkan thread baru tapi jelas "hal-hal yang kita tunggu" dapat diimplementasikan menggunakan thread.

Ada banyak hal di .NET yang tidak selalu memutar utas sendiri tetapi masih asinkron:

  • Permintaan web (dan banyak hal terkait jaringan lainnya yang membutuhkan waktu)
  • Membaca dan menulis file tidak sinkron
  • dan banyak lagi, pertanda baik adalah jika kelas / antarmuka yang dimaksud memiliki metode bernama SomethingSomethingAsyncatau BeginSomethingdan EndSomethingdan ada yang IAsyncResultterlibat.

Biasanya hal-hal ini tidak menggunakan utas di bawah tenda.


OK, jadi Anda ingin beberapa "topik luas"?

Baiklah, mari kita tanyakan pada Try Roslyn tentang klik tombol kita:

Coba Roslyn

Saya tidak akan menautkan kelas yang dibuat penuh di sini, tetapi ini adalah hal yang sangat mengerikan.

Lasse V. Karlsen
sumber
11
Jadi pada dasarnya apa yang digambarkan OP sebagai " Simulasi pelaksanaan paralel dengan menjadwalkan tugas dan beralih di antara mereka ", bukan?
Bergi
4
@Bergi Tidak cukup. Eksekusi benar-benar paralel - tugas I / O asinkron sedang berlangsung, dan tidak memerlukan utas untuk melanjutkan (ini adalah sesuatu yang telah digunakan jauh sebelum Windows muncul - MS DOS juga menggunakan asinkron I / O, meskipun tidak memiliki multi-threading!). Tentu saja, await dapat digunakan seperti yang Anda gambarkan juga, tetapi umumnya tidak. Hanya panggilan balik yang dijadwalkan (di kumpulan utas) - antara panggilan balik dan permintaan, tidak ada utas yang diperlukan.
Luaan
3
Itu sebabnya saya ingin secara eksplisit menghindari berbicara terlalu banyak tentang apa yang dilakukan metode itu, karena pertanyaannya adalah tentang async / menunggu secara khusus, yang tidak membuat utasnya sendiri. Jelas, mereka dapat digunakan untuk menunggu untuk benang untuk lengkap.
Lasse V. Karlsen
6
@ LasseV.Karlsen - Saya mencerna jawaban Anda yang hebat, tetapi saya masih menutup satu detail. Saya mengerti bahwa event handler ada, seperti pada langkah 4, yang memungkinkan pompa pesan untuk terus memompa, tetapi kapan dan di mana "hal yang membutuhkan waktu dua detik" terus dijalankan jika tidak pada utas terpisah? Jika itu dieksekusi pada utas UI, maka itu akan memblokir pompa pesan tetap saat itu mengeksekusi karena itu harus mengeksekusi beberapa waktu pada utas yang sama .. [lanjutan] ...
rory.ap
3
Saya suka penjelasan Anda dengan pompa pesan. Bagaimana penjelasan Anda berbeda ketika tidak ada pompa pesan seperti di aplikasi konsol atau server web? Bagaimana reentrace suatu metode dicapai?
Puchacz
95

Saya jelaskan secara lengkap di posting blog saya There No No Thread .

Singkatnya, sistem I / O modern banyak menggunakan DMA (Direct Memory Access). Ada prosesor khusus yang didedikasikan untuk kartu jaringan, kartu video, pengontrol HDD, port serial / paralel, dll. Prosesor ini memiliki akses langsung ke bus memori, dan menangani pembacaan / penulisan sepenuhnya terlepas dari CPU. CPU hanya perlu memberi tahu perangkat lokasi di memori yang berisi data, dan kemudian dapat melakukan hal itu sendiri hingga perangkat menimbulkan interupsi yang memberitahukan CPU bahwa proses baca / tulis selesai.

Setelah operasi dalam penerbangan, tidak ada pekerjaan untuk CPU lakukan, dan dengan demikian tidak ada utas.

Stephen Cleary
sumber
Hanya untuk memperjelas .. Saya mengerti tingkat tinggi dari apa yang terjadi ketika menggunakan async-tunggu. Mengenai pembuatan tanpa utas - tidak ada utas hanya dalam permintaan I / O ke perangkat yang seperti Anda katakan memiliki prosesor sendiri yang menangani permintaan itu sendiri? Bisakah kita mengasumsikan SEMUA permintaan I / O ditangani pada prosesor independen yang berarti menggunakan Task.RUN HANYA pada tindakan terikat CPU?
Yonatan Nir
@YonatanNir: Ini bukan hanya tentang prosesor yang terpisah; segala jenis respons yang digerakkan oleh peristiwa secara alami tidak sinkron. Task.Runpaling tepat untuk tindakan yang terikat CPU , tetapi memiliki beberapa kegunaan lain juga.
Stephen Cleary
1
Saya selesai membaca artikel Anda dan masih ada sesuatu yang mendasar yang tidak saya mengerti karena saya tidak begitu akrab dengan implementasi OS tingkat bawah. Saya mendapatkan apa yang Anda tulis hingga di mana Anda menulis: "Operasi penulisan sekarang" dalam penerbangan ". Berapa banyak utas yang memprosesnya? . Jadi jika tidak ada utas, lalu bagaimana operasi itu sendiri dilakukan jika tidak pada utas?
Yonatan Nir
6
Ini adalah bagian yang hilang dalam ribuan penjelasan !!! Sebenarnya ada seseorang yang melakukan pekerjaan di latar belakang dengan operasi I / O. Ini bukan utas tetapi komponen perangkat keras khusus lainnya melakukan tugasnya!
the_dark_destructor
2
@ PrabuWeerasinghe: Kompiler membuat struct yang menampung variabel state dan local. Jika menunggu perlu menghasilkan (yaitu, kembali ke peneleponnya), struct yang kotak dan hidup di tumpukan.
Stephen Cleary
87

satu-satunya cara agar komputer dapat melakukan lebih dari 1 hal sekaligus adalah (1) Sebenarnya melakukan lebih dari 1 hal sekaligus, (2) menyimulasikannya dengan menjadwalkan tugas dan beralih di antara mereka. Jadi jika async-tunggu, jangan lakukan keduanya

Bukannya yang menunggu tidak keduanya . Ingat, tujuannya awaitbukan untuk membuat kode sinkron secara asinkron . Ini untuk mengaktifkan menggunakan teknik yang sama yang kita gunakan untuk menulis kode sinkron ketika memanggil kode asinkron . Menunggu adalah tentang membuat kode yang menggunakan operasi latensi tinggi terlihat seperti kode yang menggunakan operasi latensi rendah . Operasi latensi tinggi itu mungkin berada di utas, mereka mungkin pada perangkat keras tujuan khusus, mereka mungkin merobek pekerjaan mereka menjadi potongan-potongan kecil dan memasukkannya ke antrian pesan untuk diproses oleh utas UI nanti. Mereka melakukan sesuatu untuk mencapai sinkronisasi, tetapi merekaadalah orang-orang yang melakukannya. Menunggu hanya memungkinkan Anda mengambil keuntungan dari sinkronisasi itu.

Juga, saya pikir Anda kehilangan opsi ketiga. Kami orang tua - anak-anak hari ini dengan musik rap mereka harus turun dari halaman saya, dll - ingat dunia Windows pada awal 1990-an. Tidak ada mesin multi-CPU dan tidak ada penjadwal thread. Anda ingin menjalankan dua aplikasi Windows secara bersamaan, Anda harus menghasilkan . Multitasking kooperatif . OS memberi tahu proses yang harus dijalankan, dan jika berperilaku buruk, OS akan membuat semua proses lainnya tidak dapat dilayani. Ini berjalan sampai menghasilkan, dan entah bagaimana ia harus tahu cara mengambil di mana ia tinggalkan saat berikutnya OS tangan mengontrol kembali ke sana. Kode asinkron single-threaded sangat mirip dengan itu, dengan "menunggu" alih-alih "menghasilkan". Menunggu berarti "Aku akan mengingat di mana aku pergi di sini, dan membiarkan orang lain lari sebentar; telepon aku kembali ketika tugas yang aku tunggu selesai, dan aku akan mengambil di tempat aku pergi." Saya pikir Anda dapat melihat bagaimana hal itu membuat aplikasi lebih responsif, seperti yang terjadi pada Windows 3 hari.

memanggil metode apa pun berarti menunggu metode selesai

Ada kunci yang Anda lewatkan. Suatu metode dapat kembali sebelum kerjanya selesai . Itulah esensi asynchrony di sana. Metode mengembalikan, mengembalikan tugas yang berarti "pekerjaan ini sedang berlangsung; katakan padaku apa yang harus dilakukan ketika sudah selesai". Pekerjaan metode ini tidak dilakukan, meskipun telah kembali .

Sebelum operator menunggu, Anda harus menulis kode yang tampak seperti spaghetti yang diulir melalui keju swiss untuk menghadapi kenyataan bahwa kami memiliki pekerjaan yang harus dilakukan setelah selesai, tetapi dengan pengembalian dan penyelesaian yang tidak sinkron . Menunggu memungkinkan Anda untuk menulis kode yang tampak seperti pengembalian dan penyelesaian disinkronkan, tanpa mereka benar - benar disinkronkan.

Eric Lippert
sumber
Bahasa modern tingkat tinggi lainnya mendukung perilaku kerja sama eksplisit yang sama, juga (yaitu fungsi melakukan beberapa hal, menghasilkan [mungkin mengirim beberapa nilai / objek ke penelepon], berlanjut di tempat yang ditinggalkan ketika kontrol dikembalikan [mungkin dengan input tambahan yang disediakan] ). Generator sangat besar di Python, untuk satu hal.
JAB
2
@ JAB: Tentu saja. Generator disebut "blok iterator" di C # dan menggunakan yieldkata kunci. Kedua asyncmetode dan iterator dalam C # adalah bentuk coroutine , yang merupakan istilah umum untuk fungsi yang tahu bagaimana menangguhkan operasinya saat ini untuk dilanjutkan kembali nanti. Sejumlah bahasa memiliki aliran coroutine atau kontrol seperti coroutine hari ini.
Eric Lippert
1
Analogi untuk menghasilkan adalah yang baik - ini adalah kerja sama multitasking dalam satu proses. (dan dengan demikian menghindari masalah stabilitas sistem multitasking koperasi seluruh sistem)
user253751
3
Saya pikir konsep "cpu interrupts" digunakan untuk IO, tidak tahu banyak tentang "programmer" modem, oleh karena itu mereka pikir utas perlu menunggu setiap bit IO.
Ian Ringrose
@EricLippert Metode Async dari WebClient sebenarnya membuat utas tambahan, lihat di sini stackoverflow.com/questions/48366871/...
KevinBui
28

Saya sangat senang seseorang mengajukan pertanyaan ini, karena untuk waktu yang lama saya juga percaya bahwa utas diperlukan untuk melakukan konkurensi. Ketika saya pertama kali melihat acara loop , saya pikir mereka bohong. Saya berpikir dalam hati, "tidak mungkin kode ini bisa bersamaan jika dijalankan dalam satu utas". Perlu diingat ini adalah setelah saya telah melalui perjuangan memahami perbedaan antara konkurensi dan paralelisme.

Setelah penelitian saya sendiri, saya akhirnya menemukan bagian yang hilang: select(). Secara khusus, IO multiplexing, dilaksanakan oleh berbagai kernel dengan nama yang berbeda: select(), poll(), epoll(), kqueue(). Ini adalah panggilan sistem yang, meskipun detail implementasi berbeda, memungkinkan Anda untuk melewatkan satu set deskriptor file untuk ditonton. Kemudian Anda dapat membuat panggilan lain yang memblokir hingga salah satu deskriptor file yang ditonton berubah.

Dengan demikian, seseorang dapat menunggu di set acara IO (loop acara utama), menangani acara pertama yang selesai, dan kemudian menghasilkan kontrol kembali ke loop acara. Bilas dan ulangi.

Bagaimana cara kerjanya? Yah, jawaban singkatnya adalah kernel dan sulap tingkat hardware. Ada banyak komponen di komputer selain CPU, dan komponen ini dapat bekerja secara paralel. Kernel dapat mengontrol perangkat ini dan berkomunikasi langsung dengan mereka untuk menerima sinyal tertentu.

Panggilan sistem multiplexing IO ini adalah blok bangunan mendasar dari loop peristiwa single-threaded seperti node.js atau Tornado. Saat Anda awaitsuatu fungsi, Anda menonton acara tertentu (penyelesaian fungsi itu), dan kemudian menghasilkan kontrol kembali ke loop acara utama. Ketika acara yang Anda tonton selesai, fungsi (akhirnya) mengambil dari tempat sebelumnya. Fungsi yang memungkinkan Anda untuk menunda dan melanjutkan perhitungan seperti ini disebut coroutine .

kepala kebun
sumber
25

awaitdan asyncgunakan Tugas bukan Utas.

Kerangka kerja ini memiliki kumpulan utas yang siap untuk menjalankan beberapa pekerjaan dalam bentuk objek Tugas ; mengirimkan Tugas ke kumpulan berarti memilih utas 1 , yang sudah ada , untuk memanggil metode tindakan tugas. Membuat Tugas adalah masalah menciptakan objek baru, jauh lebih cepat daripada membuat utas baru.

Mengingat sebuah Tugas dimungkinkan untuk melampirkan sebuah Lanjutan padanya, itu adalah objek Tugas baru yang akan dieksekusi setelah utas berakhir.

Karena async/awaitmenggunakan Tugas , mereka tidak membuat utas baru .


Sementara teknik pemrograman interupsi digunakan secara luas di setiap OS modern, saya tidak berpikir mereka relevan di sini.
Anda dapat memiliki dua tugas berikatan CPU yang dijalankan secara paralel (sebenarnya disisipkan) dalam satu CPU yang digunakan aysnc/await.
Itu tidak bisa dijelaskan hanya dengan fakta bahwa OS mendukung antrian IORP .


Terakhir kali saya memeriksa kompiler mengubah asyncmetode menjadi DFA , pekerjaan dibagi menjadi beberapa langkah, masing-masing diakhiri dengan awaitinstruksi.
The awaitdimulai nya Tugas dan melampirkannya kelanjutan untuk mengeksekusi langkah berikutnya.

Sebagai contoh konsep, berikut adalah contoh kode pseudo.
Segala sesuatunya disederhanakan demi kejelasan dan karena saya tidak ingat persis semua detailnya.

method:
   instr1                  
   instr2
   await task1
   instr3
   instr4
   await task2
   instr5
   return value

Itu bisa berubah menjadi sesuatu seperti ini

int state = 0;

Task nextStep()
{
  switch (state)
  {
     case 0:
        instr1;
        instr2;
        state = 1;

        task1.addContinuation(nextStep());
        task1.start();

        return task1;

     case 1:
        instr3;
        instr4;
        state = 2;

        task2.addContinuation(nextStep());
        task2.start();

        return task2;

     case 2:
        instr5;
        state = 0;

        task3 = new Task();
        task3.setResult(value);
        task3.setCompleted();

        return task3;
   }
}

method:
   nextStep();

1 Sebenarnya pool dapat memiliki kebijakan pembuatan tugasnya.

Margaret Bloom
sumber
16

Saya tidak akan bersaing dengan Eric Lippert atau Lasse V. Karlsen, dan yang lainnya, saya hanya ingin menarik perhatian ke sisi lain dari pertanyaan ini, yang saya pikir tidak disebutkan secara eksplisit.

Menggunakannya awaitsendiri tidak membuat aplikasi Anda responsif secara ajaib. Jika apa pun yang Anda lakukan dalam metode yang Anda tunggu dari blok thread UI, itu masih akan memblokir UI Anda dengan cara yang sama seperti versi yang tidak ditunggu-tunggu .

Anda harus menulis metode yang bisa Anda tunggu secara spesifik sehingga bisa menghasilkan thread baru atau menggunakan sesuatu seperti port penyelesaian (yang akan mengembalikan eksekusi di utas saat ini dan memanggil sesuatu yang lain untuk kelanjutan setiap kali port penyelesaian diberi sinyal). Tetapi bagian ini dijelaskan dengan baik dalam jawaban lain.

Andrew Savinykh
sumber
3
Ini bukan kompetisi di tempat pertama; ini kolaborasi!
Eric Lippert
16

Inilah cara saya melihat semua ini, mungkin tidak terlalu akurat secara teknis tetapi ini membantu saya, setidaknya :).

Pada dasarnya ada dua jenis pemrosesan (perhitungan) yang terjadi pada mesin:

  • pemrosesan yang terjadi pada CPU
  • pemrosesan yang terjadi pada prosesor lain (GPU, kartu jaringan, dll.), sebut saja mereka IO.

Jadi, ketika kita menulis sepotong kode sumber, setelah kompilasi, tergantung pada objek yang kita gunakan (dan ini sangat penting), pemrosesan akan terikat CPU , atau IO terikat , dan pada kenyataannya, itu dapat terikat pada kombinasi dari kedua.

Beberapa contoh:

  • jika saya menggunakan metode Write FileStreamobjek (yang merupakan Stream), pemrosesan akan dikatakan, 1% CPU terikat, dan 99% IO terikat.
  • jika saya menggunakan metode Write NetworkStreamobjek (yang merupakan Stream), pemrosesan akan dikatakan, 1% CPU terikat, dan 99% IO terikat.
  • jika saya menggunakan metode Write Memorystreamobjek (yang merupakan Stream), pemrosesan akan terikat CPU 100%.

Jadi, seperti yang Anda lihat, dari sudut pandang programmer berorientasi objek, meskipun saya selalu mengakses Streamobjek, apa yang terjadi di bawah ini mungkin sangat bergantung pada jenis objek.

Sekarang, untuk mengoptimalkan berbagai hal, terkadang berguna untuk dapat menjalankan kode secara paralel (perhatikan bahwa saya tidak menggunakan kata asinkron) jika memungkinkan dan / atau perlu.

Beberapa contoh:

  • Di aplikasi desktop, saya ingin mencetak dokumen, tetapi saya tidak ingin menunggu.
  • Server web saya server banyak klien pada saat yang sama, masing-masing mendapatkan halamannya secara paralel (tidak serial).

Sebelum async / menunggu, pada dasarnya kami memiliki dua solusi untuk ini:

  • Utas . Itu relatif mudah digunakan, dengan kelas Thread dan ThreadPool. Utas hanya terikat dengan CPU .
  • Model pemrograman asinkron yang "lama" Begin / End / AsyncCallback . Ini hanya model, tidak memberi tahu Anda apakah Anda akan terikat CPU atau IO. Jika Anda melihat kelas Socket atau FileStream, itu adalah IO terikat, yang keren, tetapi kami jarang menggunakannya.

Async / menunggu hanya model pemrograman yang umum, berdasarkan pada konsep Tugas . Ini sedikit lebih mudah digunakan daripada utas atau kumpulan utas untuk tugas yang terikat CPU, dan jauh lebih mudah digunakan daripada model Begin / End yang lama. Undercover, bagaimanapun, "hanya" merupakan pembungkus penuh fitur super canggih pada keduanya.

Jadi, kemenangan sebenarnya sebagian besar pada tugas-tugas IO Bound , tugas yang tidak menggunakan CPU, tetapi async / menunggu masih hanya model pemrograman, itu tidak membantu Anda untuk menentukan bagaimana / di mana pemrosesan akan terjadi pada akhirnya.

Itu berarti itu bukan karena kelas memiliki metode "DoSomethingAsync" mengembalikan objek Tugas yang dapat Anda anggap itu terikat CPU (yang berarti mungkin sangat tidak berguna , terutama jika tidak memiliki parameter token pembatalan), atau IO Bound (yang berarti itu mungkin suatu keharusan ), atau kombinasi keduanya (karena modelnya cukup viral, ikatan dan manfaat potensial dapat, pada akhirnya, super campuran dan tidak begitu jelas).

Jadi, kembali ke contoh saya, melakukan operasi Write saya menggunakan async / menunggu di MemoryStream akan tetap terikat CPU (saya mungkin tidak akan mendapat manfaat dari itu), meskipun saya pasti akan mendapat manfaat dari itu dengan file dan aliran jaringan.

Simon Mourier
sumber
1
Ini adalah jawaban yang cukup baik menggunakan theadpool untuk pekerjaan cpu terikat buruk dalam arti bahwa benang TP harus digunakan untuk membongkar operasi IO. Imo yang terikat dengan CPU harus diblokir dengan peringatan tentu saja dan tidak ada yang menghalangi penggunaan beberapa utas.
davidcarr
3

Meringkas jawaban lain:

Async / await terutama dibuat untuk tugas-tugas terikat IO karena dengan menggunakannya, orang dapat menghindari memblokir utas panggilan. Penggunaan utama mereka adalah dengan utas UI yang tidak diinginkan agar utas diblokir pada operasi terikat IO.

Async tidak membuat utas sendiri. Utas metode pemanggilan digunakan untuk menjalankan metode async hingga menemukan metode yang ditunggu. Utas yang sama kemudian melanjutkan untuk menjalankan sisa metode pemanggilan di luar pemanggilan metode async. Dalam metode async yang disebut, setelah kembali dari yang ditunggu, kelanjutan dapat dieksekusi pada utas dari kumpulan utas - satu-satunya tempat utas terpisah muncul dalam gambar.

vaibhav kumar
sumber
Ringkasan yang bagus, tapi saya pikir itu harus menjawab 2 pertanyaan lagi untuk memberikan gambaran lengkap: 1. Utas apa kode ditunggu dieksekusi pada? 2. Siapa yang mengendalikan / mengkonfigurasi kumpulan utas yang disebutkan - pengembang atau lingkungan runtime?
stojke
1. Dalam hal ini, sebagian besar kode yang ditunggu adalah operasi terikat IO yang tidak akan menggunakan utas CPU. Jika diinginkan untuk menggunakan menunggu untuk operasi terikat CPU, Tugas yang terpisah dapat muncul. 2. Utas di kumpulan utas dikelola oleh Penjadwal tugas yang merupakan bagian dari kerangka TPL.
vaibhav kumar
2

Saya coba jelaskan dari bawah. Mungkin seseorang merasa terbantu. Saya ada di sana, melakukan itu, menciptakannya kembali, ketika membuat game sederhana dalam DOS di Pascal (masa lalu yang baik ...)

Jadi ... Dalam setiap aplikasi yang digerakkan oleh peristiwa, memiliki loop acara di dalamnya yang seperti ini:

while (getMessage(out message)) // pseudo-code
{
   dispatchMessage(message); // pseudo-code
}

Kerangka kerja biasanya menyembunyikan detail ini dari Anda tetapi ada di sana. Fungsi getMessage membaca acara berikutnya dari antrian acara atau menunggu hingga terjadi peristiwa: gerakan mouse, keydown, keyup, klik, dll. Dan kemudian dispatchMessage mengirimkan acara ke pengendali acara yang sesuai. Kemudian tunggu acara berikutnya dan seterusnya hingga acara berhenti muncul yang keluar dari loop dan menyelesaikan aplikasi.

Penangan acara harus berjalan cepat sehingga loop acara dapat melakukan polling untuk lebih banyak acara dan UI tetap responsif. Apa yang terjadi jika klik tombol memicu operasi mahal seperti ini?

void expensiveOperation()
{
    for (int i = 0; i < 1000; i++)
    {
        Thread.Sleep(10);
    }
}

Nah, UI menjadi tidak responsif sampai operasi 10 detik selesai saat kontrol tetap berada dalam fungsi. Untuk mengatasi masalah ini, Anda perlu memecah tugas menjadi beberapa bagian kecil yang dapat dieksekusi dengan cepat. Ini berarti Anda tidak dapat menangani semuanya dalam satu peristiwa. Anda harus melakukan sebagian kecil dari pekerjaan, kemudian memposting acara lain ke antrian acara untuk meminta kelanjutan.

Jadi, Anda akan mengubahnya menjadi:

void expensiveOperation()
{
    doIteration(0);
}

void doIteration(int i)
{
    if (i >= 1000) return;
    Thread.Sleep(10); // Do a piece of work.
    postFunctionCallMessage(() => {doIteration(i + 1);}); // Pseudo code. 
}

Dalam hal ini hanya iterasi pertama yang berjalan, maka ia memposting pesan ke antrian acara untuk menjalankan iterasi berikutnya dan kembali. Ini contoh postFunctionCallMessagefungsi pseudo kami menempatkan acara "panggil fungsi ini" ke antrian, sehingga pengirim acara akan memanggilnya saat sudah mencapai itu. Ini memungkinkan semua acara GUI lainnya diproses sementara terus menjalankan karya yang sudah berjalan lama.

Selama tugas berjalan yang panjang ini berjalan, acara kelanjutannya selalu dalam antrian acara. Jadi pada dasarnya Anda menemukan penjadwal tugas Anda sendiri. Di mana acara kelanjutan dalam antrian adalah "proses" yang sedang berjalan. Sebenarnya ini yang dilakukan sistem operasi, kecuali bahwa pengiriman peristiwa kelanjutan dan kembali ke loop scheduler dilakukan melalui penghitung waktu CPU di mana OS mendaftarkan kode switching konteks, jadi Anda tidak perlu peduli tentang hal itu. Tapi di sini Anda menulis penjadwal Anda sendiri sehingga Anda perlu peduli tentang hal itu - sejauh ini.

Jadi kita dapat menjalankan tugas yang berjalan lama dalam satu utas paralel dengan GUI dengan memecahnya menjadi potongan-potongan kecil dan mengirimkan acara lanjutan. Ini adalah ide umum dari Taskkelas. Ini merupakan bagian dari pekerjaan dan ketika Anda memanggilnya .ContinueWith, Anda menentukan fungsi apa yang disebut sebagai bagian berikutnya ketika bagian saat ini selesai (dan nilai pengembaliannya diteruskan ke kelanjutan). The Taskkelas menggunakan kolam thread, di mana ada loop acara di setiap thread menunggu untuk melakukan potongan-potongan kerja sama dengan ingin saya menunjukkan di awal. Dengan cara ini Anda dapat memiliki jutaan tugas yang berjalan secara paralel, tetapi hanya beberapa utas untuk menjalankannya. Tetapi itu akan bekerja dengan baik hanya dengan satu utas - selama tugas Anda dibagi dengan baik menjadi beberapa bagian, masing-masing dari mereka tampak berjalan dalam parellel.

Tetapi melakukan semua ini chaining membagi pekerjaan menjadi potongan-potongan kecil secara manual adalah pekerjaan yang rumit dan benar-benar mengacaukan tata letak logika, karena seluruh kode tugas latar belakang pada dasarnya .ContinueWithberantakan. Jadi di sinilah kompiler membantu Anda. Ini melakukan semua rangkaian dan kelanjutan untuk Anda di latar belakang. Ketika Anda mengatakan awaitAnda memberi tahu kompiler bahwa "berhenti di sini, tambahkan sisa fungsi sebagai tugas lanjutan". Kompilator menangani sisanya, jadi Anda tidak perlu melakukannya.

Calmarius
sumber
0

Sebenarnya, async awaitrantai adalah mesin negara yang dihasilkan oleh kompiler CLR.

async await Namun tidak menggunakan utas yang TPL menggunakan utas untuk menjalankan tugas.

Alasan aplikasi tidak diblokir adalah bahwa mesin negara dapat memutuskan co-rutin mana yang akan dieksekusi, ulangi, periksa, dan putuskan lagi.

Bacaan lebih lanjut:

Apa yang dihasilkan async & menunggu?

Async Tunggu dan Generated StateMachine

Asinkron C # dan F # (III.): Bagaimana cara kerjanya? - Tomas Petricek

Edit :

Baik. Sepertinya uraian saya salah. Namun saya harus menunjukkan bahwa mesin negara adalah aset penting bagi async await. Sekalipun Anda menerima I / O asinkron, Anda masih memerlukan penolong untuk memeriksa apakah operasi telah selesai karena itu kami masih memerlukan mesin keadaan dan menentukan rutin mana yang dapat dijalankan secara bersamaan secara serempak.

Steve Fan
sumber
0

Ini tidak secara langsung menjawab pertanyaan, tetapi saya pikir ini adalah informasi tambahan interessting:

Async dan menunggu tidak membuat utas baru dengan sendirinya. TETAPI tergantung pada tempat Anda menggunakan async menunggu, bagian sinkron SEBELUM menunggu menunggu berjalan pada utas berbeda dari bagian sinkron SETELAH menunggu (misalnya ASP.NET dan ASP.NET inti berperilaku berbeda).

Dalam aplikasi berbasis UI-Thread (WinForms, WPF) Anda akan berada di utas yang sama sebelum dan sesudah. Tetapi ketika Anda menggunakan async jauh pada utas pool Thread, utas sebelum dan sesudah menunggu mungkin tidak sama.

Video hebat tentang topik ini

Blechdose
sumber