Mengapa bahasa pemrograman tidak secara otomatis mengelola masalah sinkron / asinkron?


27

Saya belum menemukan banyak sumber tentang ini: Saya bertanya-tanya apakah mungkin / ide yang baik untuk dapat menulis kode asinkron dengan cara sinkron.

Misalnya, berikut adalah beberapa kode JavaScript yang mengambil jumlah pengguna yang disimpan dalam database (operasi asinkron):

getNbOfUsers(function (nbOfUsers) { console.log(nbOfUsers) });

Alangkah baiknya bisa menulis sesuatu seperti ini:

const nbOfUsers = getNbOfUsers();
console.log(getNbOfUsers);

Maka kompiler akan secara otomatis menunggu respons dan kemudian mengeksekusi console.log. Itu akan selalu menunggu operasi asinkron untuk menyelesaikan sebelum hasilnya harus digunakan di tempat lain. Kami akan membuat jauh lebih sedikit menggunakan janji-janji panggilan balik, async / menunggu atau apa pun, dan tidak akan pernah perlu khawatir apakah hasil operasi tersedia segera atau tidak.

Kesalahan masih dapat dikelola (apakah nbOfUsersmendapatkan bilangan bulat atau kesalahan?) Menggunakan try / catch, atau sesuatu seperti opsional seperti dalam bahasa Swift .

Apa itu mungkin? Ini mungkin ide yang buruk / utopia ... Saya tidak tahu.


58
Saya tidak begitu mengerti pertanyaan Anda. Jika Anda "selalu menunggu operasi asinkron", maka itu bukan operasi asinkron, itu adalah operasi sinkron. Bisakah Anda mengklarifikasi? Mungkin memberikan spesifikasi jenis perilaku yang Anda cari? Juga, "apa pendapat Anda tentang hal itu" di luar topik tentang Rekayasa Perangkat Lunak . Anda perlu merumuskan pertanyaan Anda dalam konteks masalah konkret, yang memiliki jawaban tunggal, tidak ambigu, kanonik, dan benar secara objektif.
Jörg W Mittag

4
@ JörgWMittag Saya membayangkan hipotetis C # yang secara implisit awaitsa Task<T>untuk mengubahnya menjadiT
Caleth

6
Apa yang Anda usulkan tidak bisa dilakukan. Ini bukan penyusun untuk memutuskan apakah Anda ingin menunggu hasilnya atau mungkin api dan lupa. Atau jalankan di latar belakang dan tunggu nanti. Kenapa membatasi diri seperti itu?
Aneh

5
Ya, itu ide yang buruk. Cukup gunakan async/ awaitsebagai gantinya, yang membuat bagian async dari eksekusi menjadi eksplisit.
Bergi

5
Ketika Anda mengatakan bahwa dua hal terjadi secara bersamaan, Anda mengatakan bahwa boleh saja hal-hal ini terjadi dalam urutan apa pun. Jika kode Anda tidak memiliki cara untuk menjelaskan apa pemesanan ulang tidak akan melanggar harapan kode Anda, maka itu tidak dapat membuatnya bersamaan.
Rob

Jawaban:


65

Async / wait adalah manajemen otomatis yang Anda usulkan, meskipun dengan dua kata kunci tambahan. Mengapa itu penting? Selain kompatibilitas ke belakang?

  • Tanpa poin eksplisit di mana coroutine dapat ditangguhkan dan dilanjutkan, kita akan memerlukan sistem tipe untuk mendeteksi di mana nilai yang harus ditunggu harus ditunggu. Banyak bahasa pemrograman tidak memiliki sistem tipe seperti itu.

  • Dengan membuat menunggu nilai secara eksplisit, kita juga bisa memberikan nilai yang bisa ditunggu-tunggu sebagai objek kelas satu: janji. Ini bisa sangat berguna saat menulis kode tingkat tinggi.

  • Kode Async memiliki efek yang sangat dalam untuk model eksekusi suatu bahasa, mirip dengan tidak adanya atau ada pengecualian dalam bahasa tersebut. Secara khusus, fungsi async hanya bisa ditunggu oleh fungsi async. Ini memengaruhi semua fungsi panggilan! Tetapi bagaimana jika kita mengubah fungsi dari non-async ke async pada akhir rantai ketergantungan ini? Ini akan menjadi perubahan yang tidak kompatibel ke belakang ... kecuali semua fungsi async dan setiap panggilan fungsi ditunggu secara default.

    Dan itu sangat tidak diinginkan karena memiliki implikasi kinerja yang sangat buruk. Anda tidak akan dapat dengan mudah mengembalikan nilai murah. Setiap panggilan fungsi akan menjadi jauh lebih mahal.

Async memang hebat, tetapi beberapa jenis async implisit tidak akan berfungsi dalam kenyataan.

Bahasa fungsional murni seperti Haskell memiliki sedikit jalan keluar karena urutan eksekusi sebagian besar tidak ditentukan dan tidak dapat diamati. Atau diutarakan secara berbeda: urutan operasi spesifik apa pun harus dikodekan secara eksplisit. Itu bisa agak rumit untuk program dunia nyata, terutama program I / O-heavy yang kode async-nya sangat cocok.


2
Anda tidak perlu sistem jenis. Transparan Futures dalam contoh ECMAScript, Smalltalk, Self, Newspeak, Io, Ioke, Seph, dapat dengan mudah diimplementasikan tanpa dukungan sistem atau bahasa. Dalam Smalltalk dan turunannya, sebuah objek dapat mengubah identitasnya secara transparan, dalam ECMAScript, objek itu dapat mengubah bentuknya secara transparan. Hanya itu yang Anda butuhkan untuk membuat Futures transparan, tidak perlu dukungan bahasa untuk asinkron.
Jörg W Mittag

6
@ JörgWMittag Saya mengerti apa yang Anda katakan dan bagaimana itu bisa bekerja, tetapi masa depan yang transparan tanpa sistem tipe membuatnya agak sulit untuk secara bersamaan memiliki masa depan kelas satu, bukan? Saya perlu beberapa cara untuk memilih apakah saya ingin mengirim pesan ke masa depan atau nilai masa depan, lebih disukai sesuatu yang lebih baik daripada someValue ifItIsAFuture [self| self messageIWantToSend]karena itu sulit untuk diintegrasikan dengan kode generik.
amon

8
@amon "Saya bisa menulis kode async saya karena janji dan janji adalah monad." Monad sebenarnya tidak diperlukan di sini. Thunks pada dasarnya hanya janji. Karena hampir semua nilai di Haskell kotak, hampir semua nilai di Haskell sudah dijanjikan. Itu sebabnya Anda bisa melempar parcukup banyak di mana saja dalam kode Haskell murni dan mendapatkan paralellisme gratis.
DarthFennec

2
Async / menunggu mengingatkan saya pada kelanjutan monad.
les

3
Faktanya, pengecualian dan async / menunggu adalah contoh dari efek aljabar .
Alex Reinking

21

Apa yang Anda lewatkan, adalah tujuan dari operasi async: Mereka memungkinkan Anda untuk memanfaatkan waktu tunggu Anda!

Jika Anda mengubah operasi async, seperti meminta beberapa sumber daya dari server, menjadi operasi sinkron dengan secara implisit dan segera menunggu balasan, utas Anda tidak dapat melakukan hal lain dengan waktu tunggu . Jika server membutuhkan 10 milidetik untuk merespons, maka buanglah sekitar 30 juta siklus CPU. Keterlambatan respons menjadi waktu eksekusi permintaan.

Satu-satunya alasan mengapa programmer menemukan operasi async, adalah untuk menyembunyikan latensi tugas yang sudah berjalan lama di balik perhitungan berguna lainnya . Jika Anda dapat mengisi waktu tunggu dengan pekerjaan yang bermanfaat, itu menghemat waktu CPU. Jika Anda tidak bisa, yah, tidak ada yang hilang dengan operasi menjadi async.

Jadi, saya sarankan untuk merangkul operasi async yang disediakan bahasa Anda untuk Anda. Mereka ada di sana untuk menghemat waktu Anda.


saya sedang memikirkan bahasa fungsional di mana operasi tidak memblokir, jadi bahkan jika itu memiliki sintaksis sinkron, perhitungan yang berjalan lama tidak akan memblokir utas
Cinn

6
@ Cinn Saya tidak menemukan itu dalam pertanyaan, dan contoh dalam pertanyaan adalah Javascript, yang tidak memiliki fitur ini. Namun, umumnya agak sulit bagi seorang kompiler untuk menemukan peluang yang berarti untuk paralelisasi seperti yang Anda jelaskan: Eksploitasi yang berarti dari fitur seperti itu akan mengharuskan programmer untuk secara eksplisit memikirkan apa yang mereka lakukan setelah panggilan laten yang lama. Jika Anda membuat runtime cukup pintar untuk menghindari persyaratan ini pada programmer, runtime Anda mungkin akan memakan banyak penghematan kinerja karena itu perlu paralelkan secara agresif di seluruh panggilan fungsi.
cmaster

2
Semua komputer menunggu dengan kecepatan yang sama.
Bob Jarvis - Kembalikan Monica

2
@ BobJarvis Ya. Tetapi mereka berbeda dalam berapa banyak pekerjaan yang bisa mereka lakukan dalam waktu tunggu ...
cmaster

13

Beberapa melakukannya.

Mereka bukan arus utama (belum) karena async adalah fitur yang relatif baru yang baru saja kita rasakan baik-baik saja jika itu bahkan fitur yang baik, atau bagaimana menyajikannya kepada programmer dengan cara yang ramah / dapat digunakan / ekspresif / dll. Fitur async yang ada sebagian besar dibautkan ke bahasa yang ada, yang memerlukan sedikit pendekatan desain yang berbeda.

Yang mengatakan, itu jelas bukan ide yang baik untuk dilakukan di mana-mana. Kegagalan yang umum adalah melakukan panggilan async dalam satu lingkaran, secara efektif membuat serial eksekusi mereka. Memiliki panggilan tidak sinkron secara implisit dapat mengaburkan kesalahan semacam itu. Juga, jika Anda mendukung paksaan implisit dari Task<T>(atau yang setara dengan bahasa Anda) T, hal itu dapat menambah sedikit kerumitan / biaya pada typechecker Anda dan pelaporan kesalahan ketika tidak jelas yang mana dari dua yang benar-benar diinginkan oleh programmer.

Tapi itu bukan masalah yang tidak bisa diatasi. Jika Anda ingin mendukung perilaku yang hampir pasti Anda bisa, meskipun akan ada trade-off.


1
Saya pikir sebuah ide bisa jadi untuk membungkus semuanya dalam fungsi async, tugas sinkron hanya akan menyelesaikan dengan segera dan kami mendapatkan semua satu jenis untuk menangani (Edit: @amon menjelaskan mengapa itu ide yang buruk ...)
Cinn

8
Bisakah Anda memberikan beberapa contoh untuk " Some do do "?
Bergi

2
Pemrograman asinkron sama sekali bukan hal baru, hanya saja saat ini orang harus lebih sering menghadapinya.
Cubic

1
@Cubic - ini sebagai fitur bahasa sejauh yang saya tahu. Sebelumnya itu hanya (canggung) fungsi userland.
Telastyn

12

Ada bahasa yang melakukan ini. Tapi, sebenarnya tidak banyak kebutuhan, karena dapat dengan mudah dilakukan dengan fitur bahasa yang ada.

Selama Anda memiliki beberapa cara untuk mengekspresikan sinkronisasi, Anda dapat mengimplementasikan Futures atau Promises murni sebagai fitur perpustakaan, Anda tidak memerlukan fitur bahasa khusus. Dan selama Anda memiliki beberapa mengekspresikan Proxy Transparan , Anda dapat menggabungkan kedua fitur dan Anda memiliki Transparan Futures .

Misalnya, dalam Smalltalk dan turunannya, suatu objek dapat mengubah identitasnya, ia dapat secara harfiah "menjadi" objek yang berbeda (dan pada kenyataannya metode yang melakukan ini disebut Object>>become:).

Bayangkan perhitungan jangka panjang yang mengembalikan a Future<Int>. Ini Future<Int>memiliki semua metode yang sama seperti Int, kecuali dengan implementasi yang berbeda. Future<Int>'s +metode tidak menambahkan nomor lain dan mengembalikan hasil, ia mengembalikan baru Future<Int>yang membungkus perhitungan. Dan seterusnya, dan sebagainya. Metode yang tidak dapat secara bijaksana diimplementasikan dengan mengembalikan a Future<Int>, sebagai gantinya akan secara otomatis awaithasilnya, dan kemudian memanggil self become: result., yang akan membuat objek yang saat ini mengeksekusi ( self, yaitu Future<Int>) secara harfiah menjadi resultobjek, yaitu mulai sekarang pada referensi objek yang dulunya Future<Int>adalah sekarang di Intmana - mana, sepenuhnya transparan untuk klien.

Tidak diperlukan fitur bahasa terkait sinkronisasi yang tidak perlu.


Oke, tetapi ada masalah jika keduanya Future<T>dan Tberbagi beberapa antarmuka umum dan saya menggunakan fungsionalitas dari antarmuka itu. Haruskah becomehasilnya dan kemudian menggunakan fungsionalitas, atau tidak? Saya memikirkan hal-hal seperti operator kesetaraan atau representasi debugging to-string.
amon

Saya mengerti bahwa itu tidak menambahkan fitur apa pun, masalahnya kita memiliki sintaks yang berbeda untuk menulis segera menyelesaikan perhitungan dan perhitungan yang sudah berjalan lama, dan setelah itu kita akan menggunakan hasilnya dengan cara yang sama untuk tujuan lain. Saya bertanya-tanya apakah kita bisa memiliki sintaks yang menangani keduanya secara transparan, membuatnya lebih mudah dibaca sehingga programmer tidak perlu mengatasinya. Seperti melakukan a + b, kedua bilangan bulat, tidak masalah jika a dan b tersedia segera atau lambat, kami hanya menulis a + b(memungkinkan untuk melakukan Int + Future<Int>)
Cinn

@Cinn: Ya, Anda bisa melakukannya dengan Transparent Futures, dan Anda tidak memerlukan fitur bahasa khusus untuk melakukan itu. Anda dapat mengimplementasikannya menggunakan fitur yang sudah ada di misalnya Smalltalk, Self, Newspeak, Us, Korz, Io, Ioke, Seph, ECMAScript, dan tampaknya, seperti yang baru saja saya baca, Python.
Jörg W Mittag

3
@amon: Gagasan Transparent Futures adalah Anda tidak tahu ini masa depan. Dari sudut pandang Anda, tidak ada antarmuka umum antara Future<T>dan Tkarena dari sudut pandang Anda, tidak adaFuture<T> , hanya a T. Sekarang, tentu saja ada banyak tantangan teknik di sekitar bagaimana membuat ini efisien, operasi mana yang harus diblokir vs non-blocking, dll., Tetapi itu benar-benar independen apakah Anda melakukannya sebagai bahasa atau sebagai fitur perpustakaan. Transparansi adalah persyaratan yang ditetapkan oleh OP dalam pertanyaan, saya tidak akan berdebat bahwa itu sulit dan mungkin tidak masuk akal.
Jörg W Mittag

3
@ Jorg Itu sepertinya akan bermasalah dalam apa pun kecuali bahasa fungsional karena Anda tidak memiliki cara untuk mengetahui kapan kode sebenarnya dieksekusi dalam model itu. Itu umumnya berfungsi dengan baik di katakanlah Haskell, tapi saya tidak bisa melihat bagaimana ini akan bekerja dalam bahasa yang lebih prosedural (dan bahkan di Haskell, jika Anda peduli dengan kinerja Anda kadang-kadang harus memaksa eksekusi dan memahami model yang mendasarinya). Namun, ide yang menarik.
Voo

7

Mereka melakukannya (well, kebanyakan dari mereka). Fitur yang Anda cari disebut utas .

Namun utas memiliki masalah sendiri:

  1. Karena kode dapat ditangguhkan pada setiap titik , Anda tidak dapat pernah menganggap bahwa hal-hal tidak akan berubah "dengan sendirinya". Saat memprogram dengan utas, Anda membuang banyak waktu untuk memikirkan bagaimana program Anda harus berurusan dengan hal-hal yang berubah.

    Bayangkan server game memproses serangan pemain terhadap pemain lain. Sesuatu seperti ini:

    if (playerInMeleeRange(attacker, victim)) {
        const damage = calculateAttackDamage(attacker, victim);
        if (victim.health <= damage) {
    
            // attacker gets whatever the victim was carrying as loot
            const loot = victim.getInventoryItems();
            attacker.addInventoryItems(loot);
            victim.removeInventoryItems(loot);
    
            victim.sendMessage("${attacker} hits you with a ${attacker.currentWeapon} and you die!");
            victim.setDead();
        } else {
            victim.health -= damage;
            victim.sendMessage("${attacker} hits you with a ${attacker.currentWeapon}!");
        }
        attacker.markAsKiller();
    }
    

    Tiga bulan kemudian, seorang pemain menemukan bahwa dengan terbunuh dan keluar tepat saat attacker.addInventoryItemsberjalan, maka victim.removeInventoryItemsakan gagal, ia dapat menyimpan barang-barangnya dan penyerang juga mendapatkan salinan barang-barangnya. Dia melakukan ini beberapa kali, menciptakan satu juta ton emas dari udara tipis dan menghancurkan ekonomi gim.

    Atau, penyerang dapat logout saat game mengirim pesan kepada korban, dan dia tidak akan mendapatkan tag "pembunuh" di atas kepalanya, sehingga korban berikutnya tidak akan melarikan diri darinya.

  2. Karena kode dapat ditangguhkan kapan saja , Anda perlu menggunakan kunci di mana saja saat memanipulasi struktur data. Saya memberikan contoh di atas yang memiliki konsekuensi yang jelas dalam permainan, tetapi bisa lebih halus. Pertimbangkan untuk menambahkan item ke awal daftar yang ditautkan:

    newItem.nextItem = list.firstItem;
    list.firstItem = newItem;
    

    Ini bukan masalah jika Anda mengatakan bahwa utas hanya dapat ditangguhkan saat mereka melakukan I / O, dan tidak pada titik mana pun. Tapi saya yakin Anda bisa membayangkan situasi di mana ada operasi I / O - seperti logging:

    for (player = playerList.firstItem; player != null; player = item.nextPlayer) {
        debugLog("${item.name} is online, they get a gold star");
        // Oops! The player might've logged out while the log message was being written to disk, and now this will throw an exception and the remaining players won't get their gold stars.
        // Or the list might've been rearranged and some players might get two and some players might get none.
        player.addInventoryItem(InventoryItems.GoldStar);
    }
    
  3. Karena kode dapat ditangguhkan kapan saja , berpotensi ada banyak keadaan untuk disimpan. Sistem berurusan dengan ini dengan memberikan masing-masing thread tumpukan yang sama sekali terpisah. Tetapi tumpukannya cukup besar, jadi Anda tidak dapat memiliki lebih dari 2000 utas dalam program 32-bit. Atau Anda bisa mengurangi ukuran tumpukan, dengan risiko membuatnya terlalu kecil.


3

Menemukan banyak jawaban di sini menyesatkan, karena sementara pertanyaannya secara literal menanyakan tentang pemrograman asinkron dan bukan non-blocking IO, saya tidak berpikir kita dapat membahas satu tanpa mendiskusikan yang lain dalam kasus khusus ini.

Sementara pemrograman asinkron secara inheren, well, asinkron, raison d'être pemrograman asinkron sebagian besar untuk menghindari pemblokiran utas kernel. Node.js menggunakan asynchronosity melalui callback atau Promises untuk memungkinkan operasi pemblokiran dikirim dari loop peristiwa dan Netty di Jawa menggunakan asinkronitas melalui callback atau CompletableFutures untuk melakukan sesuatu yang serupa.

Namun, kode non-pemblokiran tidak memerlukan sinkronisasi . Tergantung seberapa besar bahasa pemrograman dan runtime Anda bersedia lakukan untuk Anda.

Go, Erlang, dan Haskell / GHC dapat menangani ini untuk Anda. Anda dapat menulis sesuatu seperti itu var response = http.get('example.com/test')dan melepaskannya di belakang layar sambil menunggu tanggapan. Ini dilakukan oleh goroutine, proses Erlang, atau forkIOmelepaskan utas kernel di belakang layar ketika memblokir, memungkinkannya melakukan hal-hal lain sambil menunggu respons.

Memang benar bahwa bahasa tidak dapat benar-benar menangani sinkronitas untuk Anda, tetapi beberapa abstraksi membuat Anda melangkah lebih jauh dari yang lain misalnya kelanjutan yang tidak didahulukan atau coroutine asimetris. Namun, penyebab utama kode asinkron, memblokir panggilan sistem, benar - benar dapat diabstraksi dari pengembang.

Node.js dan Java mendukung kode non-blocking asinkron , sedangkan Go dan Erlang mendukung kode non-blocking sinkron . Keduanya merupakan pendekatan yang valid dengan pengorbanan yang berbeda.

Argumen saya yang agak subyektif adalah bahwa mereka yang menentang runtime yang mengelola non-pemblokiran atas nama pengembang adalah seperti mereka yang menentang pengumpulan sampah di awal perang. Ya, itu menimbulkan biaya (dalam hal ini terutama lebih banyak memori), tetapi membuat pengembangan dan debugging lebih mudah, dan membuat basis kode lebih kuat.

Saya pribadi berpendapat bahwa kode non-blocking asinkron harus disediakan untuk pemrograman sistem di masa depan dan tumpukan teknologi yang lebih modern harus bermigrasi ke runtime non-blocking sinkron untuk pengembangan aplikasi.


1
Ini jawaban yang sangat menarik! Tapi saya tidak yakin saya mengerti perbedaan antara kode non-blocking “sinkron” dan “asinkron”. Bagi saya, kode non-pemblokiran sinkron berarti sesuatu seperti fungsi C seperti waitpid(..., WNOHANG)itu gagal jika harus diblokir. Atau apakah "sinkron" di sini berarti "tidak ada panggilan balik programmer / mesin negara / acara loop"? Tetapi untuk contoh Go Anda, saya masih harus secara eksplisit menunggu hasil dari goroutine dengan membaca dari saluran, bukan? Bagaimana ini kurang async daripada async / menunggu dalam JS / C # / Python?
amon

1
Saya menggunakan "asinkron" dan "sinkron" untuk mendiskusikan model pemrograman yang diekspos kepada pengembang dan "pemblokiran" dan "non-pemblokiran" untuk membahas pemblokiran utas kernel yang selama itu tidak dapat melakukan apa pun yang berguna, bahkan jika ada perhitungan lain yang perlu dilakukan dan ada prosesor logis cadangan yang dapat digunakan. Nah, goroutine bisa menunggu saja untuk hasilnya tanpa memblokir utas yang mendasarinya, tetapi goroutine lain dapat berkomunikasi dengannya melalui saluran jika diinginkan. Goroutine tidak perlu menggunakan saluran secara langsung untuk menunggu socket non-blocking dibaca.
Louis Jackman

Hmm ok, saya mengerti perbedaan Anda sekarang. Sedangkan saya lebih khawatir tentang mengelola data dan kontrol-aliran antara coroutine, Anda lebih khawatir tentang tidak pernah memblokir utas kernel utama. Saya tidak yakin Go atau Haskell memiliki keunggulan dibandingkan C ++ atau Java dalam hal ini karena mereka juga dapat memulai utas latar belakang, melakukannya hanya memerlukan sedikit kode lagi.
amon

@LouisJackman dapat menguraikan sedikit pada pernyataan terakhir Anda tentang async non-blocking untuk pemrograman sistem. Apa kelebihan dari pendekatan non-blocking async?
sunprophit

@sunprophit Asynchronous non-blocking hanyalah transformasi kompiler (biasanya async / menunggu), sedangkan non-blocking sinkron memerlukan dukungan runtime seperti beberapa kombinasi manipulasi stack kompleks, memasukkan titik hasil pada panggilan fungsi (yang dapat bertabrakan dengan inlining), pelacakan “ reduksi ”(membutuhkan VM seperti BEAM), dll. Seperti pengumpulan sampah, ini memperdagangkan kompleksitas runtime yang lebih sedikit untuk kemudahan penggunaan dan ketahanan. Bahasa sistem seperti C, C ++, dan Rust menghindari fitur runtime yang lebih besar seperti ini karena domain yang ditargetkan, sehingga non-blocking asinkron lebih masuk akal di sana.
Louis Jackman

2

Jika saya membaca Anda dengan benar, Anda meminta model pemrograman yang sinkron, tetapi implementasi berkinerja tinggi. Jika itu benar maka itu sudah tersedia untuk kita dalam bentuk benang hijau atau proses misalnya Erlang atau Haskell. Jadi ya, ini adalah ide yang bagus, tetapi pemasangan kembali ke bahasa yang ada tidak selalu semulus yang Anda inginkan.


2

Saya menghargai pertanyaan itu, dan mendapati bahwa sebagian besar jawaban bersifat defensif terhadap status quo. Dalam spektrum bahasa tingkat rendah hingga tingkat tinggi, kami telah terjebak dalam kebiasaan selama beberapa waktu. Tingkat yang lebih tinggi berikutnya jelas akan menjadi bahasa yang kurang fokus pada sintaksis (kebutuhan akan kata kunci eksplisit seperti menunggu dan asinkron) dan lebih banyak lagi tentang niat. (Jelas kredit untuk Charles Simonyi, tetapi memikirkan 2019 dan masa depan.)

Jika saya memberi tahu seorang programmer, menulis beberapa kode yang hanya mengambil nilai dari database, Anda dapat dengan aman menganggap maksud saya, "dan BTW, jangan menggantung UI" dan "tidak memperkenalkan pertimbangan lain yang menutupi sulit untuk menemukan bug ". Programmer masa depan, dengan generasi bahasa dan alat, pasti akan dapat menulis kode yang hanya mengambil nilai dalam satu baris kode dan pergi dari sana.

Bahasa tingkat tertinggi adalah berbicara bahasa Inggris, dan mengandalkan kompetensi pelaku tugas untuk mengetahui apa yang benar-benar ingin Anda lakukan. (Pikirkan komputer di Star Trek, atau tanyakan sesuatu dari Alexa.) Kami jauh dari itu, tetapi semakin dekat, dan harapan saya adalah bahwa bahasa / kompiler dapat lebih menghasilkan kode yang kuat dan berniat tanpa melangkah sejauh membutuhkan AI.

Di satu sisi, ada bahasa visual yang lebih baru, seperti Scratch, yang melakukan ini dan tidak macet dengan semua teknis sintaksis. Tentu saja, ada banyak pekerjaan di belakang layar terjadi sehingga programmer tidak perlu khawatir tentang hal itu. Yang mengatakan, saya tidak menulis perangkat lunak kelas bisnis di Scratch, jadi, seperti Anda, saya memiliki harapan yang sama bahwa sudah waktunya untuk bahasa pemrograman dewasa untuk secara otomatis mengelola masalah sinkron / asinkron.


1

Masalah yang Anda gambarkan adalah dua kali lipat.

  • Program yang Anda tulis harus berperilaku tidak sinkron secara keseluruhan jika dilihat dari luar .
  • Seharusnya tidak terlihat di situs panggilan apakah panggilan fungsi berpotensi menyerahkan kontrol atau tidak.

Ada beberapa cara untuk mencapai ini, tetapi mereka pada dasarnya bermuara pada

  1. memiliki banyak utas (pada tingkat abstraksi)
  2. memiliki berbagai macam fungsi di tingkat bahasa, yang semuanya disebut seperti ini foo(4, 7, bar, quux).

Untuk (1), saya menyatukan forking dan menjalankan banyak proses, menghasilkan beberapa thread kernel, dan implementasi thread hijau yang menjadwalkan thread level bahasa-runtime ke thread kernel. Dari perspektif masalah, mereka sama. Di dunia ini, tidak ada fungsi yang pernah menyerah atau kehilangan kendali dari perspektif utasnya . The benang itu sendiri kadang-kadang tidak memiliki kontrol dan kadang-kadang tidak berjalan tetapi Anda tidak menyerah kontrol thread sendiri di dunia ini. Suatu sistem yang cocok dengan model ini mungkin atau mungkin tidak memiliki kemampuan untuk menelurkan utas baru atau bergabung pada utas yang sudah ada. Sistem yang cocok dengan model ini mungkin atau mungkin tidak memiliki kemampuan untuk menduplikasi utas seperti yang dimiliki Unix fork.

(2) menarik. Untuk melakukannya keadilan kita perlu berbicara tentang formulir pengenalan dan penghapusan.

Saya akan menunjukkan mengapa implisit awaittidak dapat ditambahkan ke bahasa seperti Javascript dengan cara yang kompatibel dengan mundur. Ide dasarnya adalah bahwa dengan mengekspos janji kepada pengguna dan memiliki perbedaan antara konteks sinkron dan asinkron, Javascript telah membocorkan detail implementasi yang mencegah penanganan fungsi sinkron dan asinkron secara seragam. Ada juga fakta bahwa Anda tidak bisa awaitmenjanjikan di luar tubuh fungsi async. Pilihan desain ini tidak kompatibel dengan "membuat asinkron tidak terlihat oleh penelepon".

Anda dapat memperkenalkan fungsi sinkron menggunakan lambda dan menghilangkannya dengan panggilan fungsi.

Pengantar fungsi sinkron:

((x) => {return x + x;})

Penghapusan fungsi sinkron:

f(4)

((x) => {return x + x;})(4)

Anda dapat membandingkan ini dengan pengenalan dan eliminasi fungsi asinkron.

Pengenalan fungsi asinkron

(async (x) => {return x + x;})

Penghapusan fungsi asynchonrous (catatan: hanya valid di dalam suatu asyncfungsi)

await (async (x) => {return x + x;})(4)

Masalah mendasar di sini adalah bahwa fungsi asinkron juga merupakan fungsi sinkron yang menghasilkan objek janji .

Berikut adalah contoh memanggil fungsi asinkron secara sinkron di node.js repl.

> (async (x) => {return x + x;})(4)
Promise { 8 }

Anda secara hipotetis dapat memiliki bahasa, bahkan yang diketik secara dinamis, di mana perbedaan antara panggilan fungsi asinkron dan sinkron tidak terlihat di situs panggilan dan mungkin tidak terlihat di situs definisi.

Mengambil bahasa seperti itu dan menurunkannya ke Javascript dimungkinkan, Anda hanya perlu secara efektif membuat semua fungsi tidak sinkron.


1

Dengan goroutine Bahasa Go, dan waktu menjalankan bahasa Go, Anda dapat menulis semua kode seolah-olah itu sinkron. Jika operasi memblokir dalam satu goroutine, eksekusi berlanjut di goroutine lain. Dan dengan saluran Anda dapat berkomunikasi dengan mudah antar goroutine. Ini seringkali lebih mudah daripada callback seperti dalam Javascript atau async / menunggu dalam bahasa lain. Lihat https://tour.golang.org/concurrency/1 untuk beberapa contoh dan penjelasan.

Selain itu, saya tidak punya pengalaman pribadi dengan itu, tetapi saya mendengar Erlang memiliki fasilitas serupa.

Jadi, ya, ada bahasa pemrograman Like Go dan Erlang, yang memecahkan masalah sinkron / asinkron, tetapi sayangnya mereka belum terlalu populer. Ketika bahasa-bahasa itu semakin populer, mungkin fasilitas yang mereka sediakan akan diimplementasikan juga dalam bahasa-bahasa lain.


Saya hampir tidak pernah menggunakan bahasa Go tetapi tampaknya Anda menyatakan secara eksplisit go ..., sehingga terlihat sama seperti await ...tidak?
Cinn

1
@ Cinn Sebenarnya, tidak. Anda dapat melakukan panggilan apa pun sebagai goroutine ke serat / benang hijau sendiri go. Dan hampir semua panggilan yang mungkin diblokir dilakukan secara serempak oleh runtime, yang hanya beralih ke goroutine yang berbeda sementara itu (multi-tasking koperasi). Anda menunggu dengan menunggu pesan.
Deduplicator

2
Walaupun Goroutine adalah semacam konkurensi, saya tidak akan memasukkannya ke dalam ember yang sama dengan async / wait: bukan coroutine kooperatif tetapi secara otomatis (dan lebih dulu!) Menjadwalkan thread hijau. Tapi ini tidak membuat menunggu otomatis juga: Setara dengan Go awaitadalah membaca dari saluran <- ch.
amon

@amon Sejauh yang saya tahu, goroutine secara kooperatif dijadwalkan pada utas asli (biasanya hanya cukup untuk memaksimalkan paralelisme perangkat keras yang sebenarnya) oleh runtime, dan itu sebelumnya dijadwalkan oleh OS.
Deduplicator

OP bertanya "untuk dapat menulis kode asinkron dengan cara yang sinkron". Seperti yang telah Anda sebutkan, dengan goroutine dan go runtime, Anda dapat melakukannya. Anda tidak perlu khawatir tentang detail threading, cukup tulis pemblokiran baca dan tulis, seolah-olah kode itu sinkron, dan goroutine Anda yang lain, jika ada, akan terus berjalan. Anda juga tidak perlu "menunggu" atau membaca dari saluran untuk mendapatkan manfaat ini. Karena itu saya pikir Go adalah bahasa pemrograman yang paling mendekati keinginan OP.

1

Ada aspek yang sangat penting yang belum diangkat: reentrancy. Jika Anda memiliki kode lain (mis .: loop acara) yang berjalan selama panggilan async (dan jika Anda tidak melakukannya, mengapa Anda bahkan memerlukan async?), Maka kode tersebut dapat memengaruhi status program. Anda tidak dapat menyembunyikan panggilan async dari pemanggil karena pemanggil dapat bergantung pada bagian-bagian dari kondisi program untuk tetap tidak terpengaruh selama durasi panggilan fungsinya. Contoh:

function foo( obj ) {
    obj.x = 2;
    bar();
    log( "obj.x equals 2: " + obj.x );
}

Jika bar()fungsi async maka mungkin untuk obj.xmengubah selama eksekusi itu. Ini akan agak tidak terduga tanpa petunjuk bahwa bilah adalah async dan efek itu mungkin. Satu-satunya alternatif adalah mencurigai setiap fungsi / metode yang mungkin dilakukan sebagai async dan mengambil kembali dan memeriksa kembali keadaan non-lokal setelah setiap panggilan fungsi. Ini rentan terhadap bug halus dan bahkan mungkin tidak mungkin sama sekali jika beberapa negara non-lokal diambil melalui fungsi. Karena itu, pemrogram perlu mengetahui fungsi mana yang berpotensi mengubah status program dengan cara yang tidak terduga:

async function foo( obj ) {
    obj.x = 2;
    await bar();
    log( "obj.x equals 2: " + obj.x );
}

Sekarang jelas terlihat bahwa bar()ini adalah fungsi async, dan cara yang benar untuk menanganinya adalah memeriksa kembali nilai yang diharapkan obj.xsetelahnya dan menangani setiap perubahan yang mungkin telah terjadi.

Seperti yang sudah dicatat oleh jawaban lain, bahasa fungsional murni seperti Haskell dapat lolos dari efek itu sepenuhnya dengan menghindari kebutuhan akan keadaan bersama / global sama sekali. Saya tidak punya banyak pengalaman dengan bahasa fungsional jadi saya mungkin bias terhadapnya, tetapi saya tidak berpikir kurangnya negara global adalah keuntungan ketika menulis aplikasi yang lebih besar sekalipun.


0

Dalam kasus Javascript, yang Anda gunakan dalam pertanyaan Anda, ada poin penting yang harus diperhatikan: Javascript adalah single-threaded, dan urutan eksekusi dijamin selama tidak ada panggilan async.

Jadi jika Anda memiliki urutan seperti milik Anda:

const nbOfUsers = getNbOfUsers();

Anda dijamin tidak akan melakukan eksekusi. Tidak perlu kunci atau yang serupa.

Namun, jika getNbOfUserstidak sinkron, maka:

const nbOfUsers = await getNbOfUsers();

berarti saat getNbOfUsersdijalankan, hasil eksekusi, dan kode lain dapat berjalan di antaranya. Ini pada gilirannya mungkin memerlukan penguncian, tergantung pada apa yang Anda lakukan.

Jadi, ide yang baik untuk diperhatikan ketika panggilan tidak sinkron dan ketika tidak, karena dalam beberapa situasi Anda perlu mengambil tindakan pencegahan tambahan yang tidak perlu dilakukan jika panggilan itu sinkron.


Anda benar, kode kedua saya dalam pertanyaan tidak valid seolah getNbOfUsers()mengembalikan Janji. Tapi itu persis titik pertanyaan saya, mengapa kita perlu secara eksplisit menuliskannya sebagai asinkron, kompiler dapat mendeteksi dan menanganinya secara otomatis dengan cara yang berbeda.
Cinn

@ Cinn bukan itu maksud saya. Maksud saya adalah bahwa alur eksekusi dapat sampai ke bagian lain dari kode Anda selama eksekusi panggilan asinkron, sementara itu tidak mungkin untuk panggilan sinkron. Itu akan seperti memiliki beberapa utas yang berjalan tetapi tidak menyadarinya. Ini bisa berakhir pada masalah besar (yang biasanya sulit dideteksi dan diperbanyak).
jcaron

-4

Ini tersedia dalam C ++ std::asyncsejak C ++ 11.

Fungsi templat async menjalankan fungsi f secara asinkron (berpotensi dalam utas terpisah yang mungkin menjadi bagian dari kumpulan utas) dan mengembalikan std :: future yang pada akhirnya akan menampung hasil dari pemanggilan fungsi itu.

Dan dengan C ++ 20 coroutine dapat digunakan:


5
Ini sepertinya tidak menjawab pertanyaan. Menurut tautan Anda: "Apa yang diberikan Coroutines TS kepada kami? Tiga kata kunci bahasa baru: co_await, co_yield, dan co_return" ... Tetapi pertanyaannya adalah mengapa kami memerlukan kata kunci await(atau co_awaitdalam hal ini) di tempat pertama?
Arturo Torres Sánchez
Dengan menggunakan situs kami, Anda mengakui telah membaca dan memahami Kebijakan Cookie dan Kebijakan Privasi kami.
Licensed under cc by-sa 3.0 with attribution required.