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?
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?
Jawaban:
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.
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.
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:
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.
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 .
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.
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 await
di dalamnya. Ini pada dasarnya adalah hasil, yang menghasilkan eksekusi loop
yang kemudian memungkinkan fungsi yang berbeda untuk dijalankan (dalam hal ini, factorial
fungsi 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 #