Mengapa coroutine kembali? [Tutup]

19

Sebagian besar pekerjaan dasar untuk coroutine terjadi pada 60-an / 70-an dan kemudian berhenti mendukung alternatif (misalnya, utas)

Apakah ada substansi pada minat baru pada coroutine yang telah terjadi dalam python dan bahasa lain?

pengguna1787812
sumber
9
Saya tidak yakin mereka pernah pergi.
Blrfl

Jawaban:

26

Coroutine tidak pernah pergi, sementara itu mereka hanya dibayangi oleh hal-hal lain. Minat yang meningkat baru-baru ini pada pemrograman asinkron dan karena itu coroutine sebagian besar disebabkan oleh tiga faktor: peningkatan penerimaan teknik pemrograman fungsional, toolets dengan dukungan buruk untuk paralelisme sejati (JavaScript! Python!), Dan yang paling penting: perbedaan timbal balik antara thread dan coroutine. Untuk beberapa kasus penggunaan, coroutine secara objektif lebih baik.

Salah satu paradigma pemrograman terbesar tahun 80-an, 90-an dan hari ini adalah OOP. Jika kita melihat sejarah OOP dan khususnya pada pengembangan bahasa Simula, kita melihat bahwa kelas berevolusi dari coroutine. Simula dimaksudkan untuk simulasi sistem dengan kejadian diskrit. Setiap elemen sistem adalah proses terpisah yang akan dieksekusi sebagai respons terhadap peristiwa selama satu langkah simulasi, kemudian menghasilkan untuk membiarkan proses lain melakukan pekerjaan mereka. Selama pengembangan Simula 67 konsep kelas diperkenalkan. Sekarang keadaan persisten coroutine disimpan di anggota objek, dan peristiwa dipicu dengan memanggil metode. Untuk lebih jelasnya, pertimbangkan membaca makalah Pengembangan bahasa SIMULA oleh Nygaard & Dahl.

Jadi dalam twist yang lucu, kami telah menggunakan coroutine selama ini, kami hanya memanggil mereka objek dan pemrograman berbasis peristiwa.

Sehubungan dengan paralelisme, ada dua jenis bahasa: yang memiliki model memori yang tepat, dan yang tidak. Model memori membahas hal-hal seperti "Jika saya menulis ke variabel dan setelah itu membaca dari variabel itu di utas lain, apakah saya melihat nilai lama atau nilai baru atau mungkin nilai yang tidak valid? Apa yang dimaksud dengan 'sebelum' dan 'setelah'? Operasi mana yang dijamin atom? "

Membuat model memori yang baik itu sulit, jadi upaya ini tidak pernah dilakukan untuk sebagian besar bahasa sumber terbuka dinamis yang tidak ditentukan dan ditentukan implementasi: Perl, JavaScript, Python, Ruby, PHP. Tentu saja, semua bahasa itu berkembang jauh melampaui "scripting" yang awalnya mereka buat. Ya, beberapa bahasa ini memang memiliki semacam dokumen model memori, tetapi itu tidak cukup. Sebaliknya, kami memiliki peretasan:

  • Perl dapat dikompilasi dengan dukungan threading, tetapi setiap utas berisi klon terpisah dari status juru bahasa lengkap, membuat utas menjadi sangat mahal. Sebagai satu-satunya manfaat, pendekatan berbagi-apa-apa ini menghindari perlombaan data, dan memaksa programmer untuk berkomunikasi hanya melalui antrian / sinyal / IPC. Perl tidak memiliki cerita yang kuat untuk pemrosesan async.

  • JavaScript selalu memiliki dukungan yang kaya untuk pemrograman fungsional, sehingga programmer akan secara manual menyandikan kelanjutan / panggilan balik dalam program mereka di mana mereka membutuhkan operasi asinkron. Misalnya, dengan permintaan Ajax atau penundaan animasi. Karena web pada dasarnya async, ada banyak kode JavaScript async dan mengatur semua panggilan balik ini sangat menyakitkan. Karena itu kami melihat banyak upaya untuk mengatur callback tersebut dengan lebih baik (Janji) atau untuk menghilangkannya sepenuhnya.

  • Python memiliki fitur malang ini yang disebut Global Interpreter Lock. Pada dasarnya model memori Python adalah “Semua efek muncul berurutan karena tidak ada paralelisme. Hanya satu utas yang akan menjalankan kode Python sekaligus. ”Jadi, sementara Python memang memiliki utas, ini hanya sekuat coroutine. [1] Python dapat menyandikan banyak coroutine melalui fungsi generator dengan yield. Jika digunakan dengan benar, ini saja dapat menghindari sebagian besar panggilan balik yang diketahui dari JavaScript. Sistem async / await yang lebih baru dari Python 3.5 membuat idiom asinkron lebih nyaman digunakan di Python, dan mengintegrasikan loop peristiwa.

    [1]: Secara teknis pembatasan ini hanya berlaku untuk CPython, implementasi referensi Python. Implementasi lain seperti Jython memang menawarkan untaian nyata yang dapat dieksekusi secara paralel, tetapi harus melalui upaya besar untuk mengimplementasikan perilaku yang setara. Intinya: setiap variabel atau anggota objek adalah variabel volatil sehingga semua perubahan bersifat atomik dan langsung terlihat di semua utas. Tentu saja, menggunakan variabel volatil jauh lebih mahal daripada menggunakan variabel normal.

  • Saya tidak cukup tahu tentang Ruby dan PHP untuk memanggangnya dengan benar.

Untuk meringkas: beberapa bahasa ini memiliki keputusan desain mendasar yang membuat multithreading tidak diinginkan atau tidak mungkin, mengarah pada fokus yang lebih kuat pada alternatif seperti coroutine dan pada cara-cara untuk membuat pemrograman async lebih nyaman.

Akhirnya, mari kita bicara tentang perbedaan antara coroutine dan utas:

Utas pada dasarnya seperti proses, kecuali beberapa utas di dalam suatu proses berbagi ruang memori. Ini berarti utas tidak berarti “ringan” dalam hal memori. Utas sudah dijadwalkan sebelumnya oleh sistem operasi. Ini berarti sakelar tugas memiliki overhead yang tinggi, dan dapat terjadi pada waktu yang tidak nyaman. Overhead ini memiliki dua komponen: biaya menangguhkan status utas, dan biaya beralih antara mode pengguna (untuk utas) dan mode kernel (untuk penjadwal).

Jika suatu proses menjadwalkan utasnya sendiri secara langsung dan kooperatif, konteks beralih ke mode kernel tidak diperlukan, dan mengalihkan tugas relatif mahal ke pemanggilan fungsi tidak langsung, seperti pada: cukup murah. Benang ringan ini dapat disebut benang hijau, serat, atau coroutine tergantung pada berbagai detail. Pengguna terkemuka benang hijau / serat adalah implementasi Java awal, dan baru-baru ini Goroutine di Golang. Keuntungan konseptual coroutine adalah pelaksanaannya dapat dipahami dalam hal aliran kontrol yang secara eksplisit berpindah-pindah antar coroutine. Namun, coroutine ini tidak mencapai paralelisme yang sebenarnya kecuali mereka dijadwalkan di beberapa utas OS.

Di mana coroutine murah bermanfaat? Sebagian besar perangkat lunak tidak memerlukan trilyun utas, jadi utas mahal yang normal biasanya OK. Namun, pemrograman async terkadang dapat menyederhanakan kode Anda. Untuk digunakan secara bebas, abstraksi ini harus cukup murah.

Lalu ada web. Seperti disebutkan di atas, web pada dasarnya asinkron. Permintaan jaringan cukup lama. Banyak server web mempertahankan kumpulan utas yang penuh dengan utas pekerja. Namun, sebagian besar waktu mereka utas ini akan menganggur karena mereka menunggu beberapa sumber daya, baik itu menunggu acara I / O ketika memuat file dari disk, menunggu sampai klien telah mengakui sebagian dari respons, atau menunggu sampai database permintaan selesai. NodeJS telah menunjukkan secara fenomenal bahwa desain server berdasarkan kejadian dan asinkron berfungsi dengan sangat baik. Jelas JavaScript jauh dari satu-satunya bahasa yang digunakan untuk aplikasi web, jadi ada juga insentif besar untuk bahasa lain (terlihat dalam Python dan C #) untuk membuat pemrograman web asinkron lebih mudah.

amon
sumber
Saya akan merekomendasikan sumber paragraf keempat ke terakhir Anda untuk menghindari risiko plagiarisme, hampir persis sama dengan sumber lain yang saya baca. Selain itu, walaupun memiliki pesanan yang lebih besar daripada overhead thread, kinerja coroutine tidak dapat disederhanakan menjadi "pemanggilan fungsi tidak langsung". Lihat Meningkatkan rincian tentang implementasi coroutine di sini , dan di sini .
whn
1
@snb Mengenai hasil edit yang Anda sarankan: GIL mungkin merupakan detail implementasi CPython, tetapi masalah mendasarnya adalah bahwa bahasa Python tidak memiliki model memori eksplisit yang menentukan mutasi paralel data. GIL adalah peretasan untuk menghindari masalah ini. Tetapi implementasi Python dengan paralelisme yang benar harus melalui upaya besar untuk menyediakan semantik yang setara, misalnya seperti yang dibahas dalam buku Jython . Pada dasarnya: setiap variabel atau bidang objek harus berupa variabel volatil yang mahal .
amon
3
@snb Mengenai plagiarisme: Plagiarisme secara salah menghadirkan ide Anda, terutama dalam konteks akademik. Ini tuduhan serius , tapi saya yakin Anda tidak bermaksud seperti itu. Paragraf "Utas pada dasarnya seperti proses" hanya mengulangi fakta-fakta terkenal seperti yang diajarkan dalam kuliah atau buku teks tentang sistem operasi. Karena hanya ada begitu banyak cara untuk secara ringkas mengutarakan fakta-fakta ini, saya tidak terkejut paragraf itu terdengar asing bagi Anda.
amon
Saya tidak mengubah arti untuk menyiratkan bahwa Python memang memiliki model memori. Juga penggunaan volatile tidak dengan sendirinya menurunkan volatilitas kinerja hanya berarti kompiler tidak dapat mengoptimalkan variabel dengan cara dapat menganggap variabel tidak akan berubah tanpa operasi eksplisit dalam konteks saat ini. Di dunia Jython ini mungkin sebenarnya penting, karena akan menggunakan kompilasi VM JIT, tetapi di dunia CPython Anda tidak khawatir tentang optimasi JIT, variabel volatil Anda akan ada di ruang runtime juru bahasa, di mana tidak ada optimasi yang dapat dilakukan .
whn
7

Coroutine dulu berguna karena sistem operasi tidak melakukan penjadwalan pre-emptive . Setelah mereka mulai menyediakan penjadwalan pre-emptive, itu perlu lagi untuk menyerahkan kontrol secara berkala dalam program Anda.

Ketika prosesor multi-core menjadi lebih lazim, coroutine digunakan untuk mencapai paralelisme tugas dan / atau menjaga pemanfaatan sistem tetap tinggi (ketika satu utas eksekusi harus menunggu pada sumber daya, yang lain dapat mulai berjalan di tempatnya).

NodeJS adalah kasus khusus, di mana coroutine digunakan mendapatkan akses paralel ke IO. Yaitu, beberapa utas digunakan untuk melayani permintaan IO, tetapi utas tunggal digunakan untuk mengeksekusi kode javascript. Tujuan mengeksekusi kode pengguna di thread signle adalah untuk menghindari perlunya menggunakan mutex. Ini termasuk dalam kategori mencoba untuk menjaga pemanfaatan sistem tetap tinggi seperti yang disebutkan di atas.

dlasalle
sumber
4
Tetapi coroutine tidak dikelola dengan OS. OS tidak tahu apa coroutine itu, tidak seperti serat C ++
overexchange
Banyak OS memiliki coroutine.
Jörg W Mittag
coroutine seperti python dan Javascript ES6 + bukan multi-proses? Bagaimana mereka mencapai paralelisme tugas?
whn
1
@Mael "Kebangkitan" baru-baru ini dari coroutine berasal dari python dan javascript, yang keduanya tidak mencapai paralelisme dengan coroutine mereka seperti yang saya mengerti. Dengan kata lain jawaban ini salah, karena tugas parrallisme bukanlah alasan mengapa coroutine "kembali" sama sekali. Juga Luas juga bukan multiproses? EDIT: Saya baru sadar Anda tidak berbicara tentang paralelisme, tetapi mengapa Anda membalas saya sejak awal? Membalas dlasalle, karena jelas mereka salah tentang ini.
whn
3
@dlasalle Tidak mereka tidak bisa terlepas dari kenyataan bahwa ia mengatakan "berjalan secara paralel" itu tidak berarti kode apa pun dijalankan secara fisik pada saat yang sama. GIL akan menghentikannya dan async tidak menelurkan proses terpisah yang diperlukan untuk multiprosesor dalam CPython (GIL terpisah). Async bekerja dengan hasil pada satu utas. Ketika mereka mengatakan "parralel" mereka sebenarnya berarti beberapa fungsi untuk membangun fungsi lain bekerja dan pelaksanaan fungsi interleving . Proses async Python tidak dapat dijalankan secara paralel karena impl. Saya sekarang memiliki tiga bahasa yang tidak melakukan parralel coroutines, Lua, Javascript, dan Python.
whn
5

Sistem awal menggunakan coroutine untuk menyediakan konkurensi terutama karena mereka adalah cara paling sederhana untuk melakukannya. Utas memerlukan cukup banyak dukungan dari sistem operasi (Anda dapat menerapkannya di tingkat pengguna, tetapi Anda akan memerlukan beberapa cara mengatur agar sistem secara berkala mengganggu proses Anda) dan lebih sulit untuk diterapkan meskipun Anda memang memiliki dukungan .

Thread mulai mengambil alih nanti karena, pada tahun 70-an atau 80-an semua sistem operasi yang serius mendukung mereka (dan, pada tahun 90-an, bahkan Windows!), Dan mereka lebih umum. Dan mereka lebih mudah digunakan. Tiba-tiba semua orang mengira utas adalah hal besar berikutnya.

Pada akhir tahun 90an, retakan mulai muncul, dan selama awal tahun 2000-an menjadi jelas bahwa ada masalah serius dengan utas:

  1. mereka menghabiskan banyak sumber daya
  2. sakelar konteks membutuhkan banyak waktu, relatif berbicara, dan seringkali tidak perlu
  3. mereka menghancurkan referensi lokal
  4. menulis kode yang benar yang mengoordinasi banyak sumber daya yang mungkin memerlukan akses eksklusif sangat sulit

Seiring waktu, jumlah tugas yang biasanya perlu dilakukan oleh program setiap saat telah berkembang pesat, meningkatkan masalah yang disebabkan oleh (1) dan (2) di atas. Perbedaan antara kecepatan prosesor dan waktu akses memori telah meningkat, memperburuk masalah (3). Dan kompleksitas program dalam hal berapa banyak dan berbagai macam sumber daya yang mereka butuhkan telah tumbuh, meningkatkan relevansi masalah (4).

Tetapi dengan kehilangan sedikit sifat umum, dan menempatkan sedikit tanggung jawab tambahan pada programmer untuk berpikir tentang bagaimana proses mereka dapat beroperasi bersama, coroutine dapat menyelesaikan semua masalah ini.

  1. Coroutine membutuhkan sedikit sumber daya lebih bijaksana daripada beberapa halaman untuk stack, jauh lebih sedikit daripada kebanyakan implementasi utas.
  2. Coroutines hanya mengalihkan konteks pada titik-titik yang ditentukan pemrogram, yang semoga hanya berarti bila perlu. Mereka juga biasanya tidak perlu menyimpan sebanyak mungkin informasi konteks (mis. Nilai register) seperti halnya thread, yang berarti setiap switch biasanya lebih cepat dan juga membutuhkan lebih sedikit.
  3. Pola coroutine yang umum termasuk operasi tipe produsen / konsumen menyerahkan data antar rutin dengan cara yang secara aktif meningkatkan lokalitas. Selain itu, sakelar konteks biasanya hanya terjadi di antara unit kerja yang tidak ada di dalamnya, yaitu pada saat lokalitas biasanya diminimalkan.
  4. Penguncian sumber daya kurang mungkin diperlukan ketika rutin tahu bahwa mereka tidak dapat secara sewenang-wenang terganggu di tengah operasi, memungkinkan implementasi yang lebih sederhana untuk bekerja dengan benar.
Jules
sumber
5

Kata pengantar

Saya ingin memulai dengan menyatakan alasan mengapa coroutine tidak mendapatkan kebangkitan, paralelisme. Secara umum coroutine modern bukanlah sarana untuk mencapai paralelisme berbasis tugas, karena implementasi modern tidak menggunakan fungsi multi-pemrosesan. Hal terdekat yang Anda dapatkan adalah hal-hal seperti serat .

Penggunaan Modern (mengapa mereka kembali)

Coroutine modern telah datang sebagai cara untuk mencapai evaluasi malas , sesuatu yang sangat berguna dalam bahasa fungsional seperti haskell, di mana dengan alih-alih mengulangi seluruh rangkaian untuk melakukan operasi, Anda akan dapat melakukan evaluasi operasi hanya sebanyak yang diperlukan ( berguna untuk set item tak terbatas atau set besar dengan terminasi dan subset awal).

Dengan penggunaan kata kunci Yield untuk membuat generator (yang dengan sendirinya memenuhi bagian dari kebutuhan evaluasi malas) dalam bahasa seperti Python dan C #, coroutine, dalam implementasi modern tidak hanya mungkin, tetapi mungkin tanpa sintaks khusus dalam bahasa itu sendiri (meskipun python akhirnya menambahkan beberapa bit untuk membantu). Co-rutin membantu dengan evaulation malas dengan ide masa depan di mana jika Anda tidak membutuhkan nilai variabel pada waktu itu, Anda dapat menunda untuk benar-benar memperolehnya sampai Anda secara eksplisit meminta nilai itu (memungkinkan Anda untuk menggunakan nilai dan malas mengevaluasinya pada waktu yang berbeda dari instantiation).

Selain evaluasi malas, terutama di websphere, rutinitas ini membantu memperbaiki panggilan balik neraka . Coroutine menjadi berguna dalam akses basis data, transaksi online, dll, di mana waktu pemrosesan pada mesin klien itu sendiri tidak akan menghasilkan akses yang lebih cepat dari apa yang Anda butuhkan. Threading dapat memenuhi hal yang sama, tetapi membutuhkan lebih banyak overhead dalam bidang ini, dan berbeda dengan coroutine, sebenarnya berguna untuk paralelisme tugas .

Singkatnya, seiring perkembangan web dan paradigma fungsional bergabung lebih banyak dengan bahasa imperatif, coroutine telah datang sebagai solusi untuk masalah asinkron dan evaluasi malas. Coroutine datang ke ruang masalah di mana threading multiprocess dan threading pada umumnya tidak perlu, tidak nyaman atau tidak mungkin.

Contoh Modern

Coroutine dalam bahasa-bahasa seperti Javascript, Lua, C # dan Python semuanya menurunkan implementasinya dengan fungsi individual yang menyerahkan kontrol utas utama ke fungsi lain (tidak ada hubungannya dengan panggilan sistem operasi).

Di contoh python ini , kita memiliki fungsi python yang lucu dengan sesuatu yang disebut awaitdi dalamnya. Ini pada dasarnya adalah hasil, yang menghasilkan eksekusi loopyang kemudian memungkinkan fungsi yang berbeda untuk dijalankan (dalam hal ini, factorialfungsi yang berbeda ). Perhatikan bahwa ketika dikatakan "Eksekusi paralel tugas" yang keliru, sebenarnya tidak dieksekusi secara paralel, eksekusi fungsi interleaving -nya melalui penggunaan kata kunci yang menunggu (yang perlu diingat hanyalah jenis hasil khusus)

Mereka memungkinkan hasil kontrol tunggal, non paralel proses bersamaan yang bukan tugas paralel , dalam arti bahwa tugas-tugas ini tidak pernah beroperasi pada saat yang sama. Coroutine bukan utas dalam implementasi bahasa modern. Semua bahasa ini implementasi rutinitas turunan berasal dari panggilan fungsi hasil ini (yang Anda programmer harus benar-benar dimasukkan secara manual ke rutinitas bersama Anda).

EDIT: C ++ Meningkatkan coroutine2 bekerja dengan cara yang sama, dan penjelasan mereka harus memberikan visual yang lebih baik dari apa yang saya bicarakan dengan kamu, lihat di sini . Seperti yang Anda lihat, tidak ada "kasus khusus" dengan implementasi, hal-hal seperti meningkatkan serat adalah pengecualian dari aturan, dan bahkan kemudian memerlukan sinkronisasi eksplisit.

EDIT2: karena seseorang mengira saya berbicara tentang sistem berbasis tugas c, saya tidak. Saya berbicara tentang sistem Unity dan naif implementasi c #

wih
sumber
@ T.Sar Saya tidak pernah menyatakan C # memiliki coroutine "alami", juga C ++ (mungkin berubah) juga python (dan masih memilikinya), dan ketiganya memiliki implementasi rutin co. Tetapi semua implementasi C # dari coroutine (seperti yang ada dalam kesatuan) didasarkan pada hasil seperti yang saya jelaskan. Juga penggunaan "hack" Anda di sini tidak ada artinya, saya kira setiap program adalah hack karena tidak selalu didefinisikan dalam bahasa. Saya sama sekali tidak mencampur C # "sistem berbasis tugas" dengan apa pun, saya bahkan tidak menyebutkannya.
whn
Saya sarankan membuat jawaban Anda sedikit lebih jelas. C # memiliki konsep menunggu instruksi dan sistem paralelisme berbasis tugas - menggunakan C # dan kata-kata tersebut sambil memberikan contoh pada python tentang bagaimana python tidak benar-benar paralel dapat menyebabkan banyak kebingungan. Juga, hapus kalimat pertama Anda - tidak perlu menyerang langsung pengguna lain dalam jawaban seperti itu.
T. Sar - Pasang kembali Monica