2.17

View in English

2.17 Konkurensi dan paralelisme

Tinjauan dan motivasi

Konkurensi adalah seni menstrukturkan program sebagai tugas independen yang dapat maju tanpa saling menunggu. Paralelisme adalah benar-benar mengeksekusi tugas-tugas itu pada saat yang sama di beberapa prosesor. Perbedaannya bukan pedantik. Konkurensi adalah cara mengorganisasi kode agar panggilan jaringan yang lambat tidak membekukan seluruh program; paralelisme adalah cara menyelesaikan komputasi besar lebih cepat dengan menyebarkannya ke seluruh inti. Mencampuradukkan keduanya membuat tim menambah thread berharap kecepatan dan hanya menerima bug.

Bagi tim besar, topik ini penting karena konkurensi adalah tempat kebenaran diam-diam mati. Satu penulis yang menulis kode berthread tunggal dapat bernalar tentangnya baris demi baris, tetapi begitu banyak penulis berbagi memori antarthread, jumlah interleaving yang mungkin meledak, dan program yang lolos setiap tes tetap dapat gagal sekali dalam sejuta di bawah beban produksi, bukan dengan crash yang keras melainkan dengan data rusak, permintaan menggantung, dan insiden yang tak dapat direproduksi siapa pun. Bab ini membangun di atas fokus tingkat kode bab 2.16 (rekayasa kinerja) dan fondasi komputasi bab 2.13, dan memberi makan masalah koordinasi bab 3.3 (sistem terdistribusi), yaitu konkurensi lintas mesin dengan kekejaman tambahan berupa jaringan yang tidak andal.

Bagi enterprise, bug konkurensi adalah bug throughput. Layanan berlalu lintas tinggi hidup atau mati berdasarkan kemampuannya menangani ribuan permintaan simultan tanpa balapan pada status bersama, dan satu penghitung tak tersinkronisasi dapat merusak buku besar di bawah beban. Bagi pemerintah, taruhannya adalah kebenaran dan kemampuan diaudit dalam sistem yang berjalan puluhan tahun dan menyentuh keselamatan, tunjangan, atau catatan publik. Race di sistem pajak atau kesehatan bukan ketidaknyamanan; itu jawaban keliru yang harus kelak dijelaskan seseorang kepada badan pengawas. Di kedua lingkungan, tujuannya sama: jadikan jalur aman sebagai bawaan, agar banyak orang yang menyentuh kode tidak masing-masing harus menjadi pakar konkurensi.

Prinsip utama

  • Konkurensi adalah struktur; paralelisme adalah eksekusi. Putuskan mana yang benar-benar Anda butuhkan sebelum meraih thread.
  • Status mutabel bersama adalah musuh. Hampir setiap bug konkurensi bermuara pada dua tugas menyentuh data yang dapat berubah yang sama.
  • Pilih imutabilitas dan penyampaian pesan. Data yang tidak dapat berubah tidak dapat diperebutkan, dan pesan mengalahkan memori bersama dalam keamanan.
  • Nondeterminisme adalah kesulitan inti. Bug yang muncul satu eksekusi dalam seribu adalah seluruh masalah, bukan kasus tepi.
  • Batasi segalanya. Antrean, jumlah thread, dan pekerjaan dalam proses yang tak terbatas mengubah lonjakan menjadi pemadaman.
  • Model tingkat lebih tinggi mengalahkan lock mentah. Aktor, kanal, dan konkurensi terstruktur memberi banyak penulis bawaan yang aman, dan setiap lock punya biaya.
  • Uji interleaving, bukan hanya jalur bahagia. Tes deterministik tidak dapat menangkap bug yang hanya diungkap urutan langka.

Rekomendasi

Putuskan apakah Anda butuh konkurensi atau paralelisme

Mulailah dengan menamai masalahnya. Jika layanan Anda menghabiskan sebagian besar waktunya menunggu (basis data, panggilan jaringan, atau disk), Anda memiliki beban kerja terikat I/O, dan konkurensi adalah jawabannya: strukturkan kode agar selagi satu permintaan menunggu, yang lain maju. Satu thread dengan async/await, atau kolam kecil, dapat melayani ribuan permintaan yang menunggu. Jika sebaliknya program Anda terikat CPU, menggiling komputasi dengan sedikit menunggu, maka paralelisme di seluruh inti adalah yang membeli kecepatan, dan di sini langit-langitnya ditetapkan hukum Amdahl (lihat bab 2.16): bagian serial membatasi percepatan Anda berapa pun inti yang Anda tambah. Ukur rezim mana yang Anda tempati sebelum merancang.

Perlakukan status mutabel bersama sebagai musuh

Hampir setiap cacat konkurensi mereduksi ke bentuk yang sama: dua tugas membaca dan menulis data yang dapat berubah yang sama tanpa menyepakati urutan. Ini adalah race condition, dan menghasilkan pembaruan hilang, objek setengah tertulis, dan nilai yang melanggar invarian yang diasumsikan kode aman. Pertahanan paling andal adalah memiliki lebih sedikit status mutabel bersama. Beri setiap tugas datanya sendiri, oper salinan alih-alih referensi, dan batasi status mutabel pada satu pemilik yang dijangkau pihak lain lewat pesan. Ketika Anda benar-benar harus berbagi, buat pembagian itu eksplisit dan kecil, agar peninjau dapat melihat setiap tempat status disentuh.

Pilih imutabilitas dan penyampaian pesan sebagai bawaan

Data bersama paling aman adalah data yang tidak dapat berubah. Objek imutabel, setelah dibangun, dapat dibaca sejumlah thread tanpa sinkronisasi sama sekali, karena tidak ada yang bisa diperebutkan. Jadikan imutabilitas bawaan Anda dan mutabilitas pengecualian yang disengaja. Ketika tugas harus berkoordinasi, pilih penyampaian pesan daripada memori bersama: alih-alih berbagi variabel umum, minta satu tugas mengirim nilai ke yang lain, yang merupakan filosofi di balik pepatah Go “jangan berkomunikasi dengan berbagi memori; berbagi memori dengan berkomunikasi.” Penyampaian pesan mengubah bug tak terlihat dan bergantung urutan menjadi aliran data eksplisit yang dapat diperiksa, dan kejelasan itu hampir selalu sepadan dengan biaya per pesannya dalam kode yang dipelihara banyak orang.

Raih model tingkat lebih tinggi sebelum lock mentah

Penguncian tulisan tangan benar dalam prinsip dan bencana dalam praktik, karena manusia buruk bernalar tentang setiap interleaving. Pilih model yang menjadikan konkurensi aman sebagai bawaan. Model aktor memberi setiap aktor status pribadi dan kotak surat: aktor tidak pernah berbagi memori dan hanya mengirim pesan, sehingga seluruh kelas race lenyap. Communicating sequential processes (CSP), model di balik kanal dalam bahasa seperti Go, membuat proses independen mengoper nilai lewat kanal bertipe. Konkurensi terstruktur mengikat umur tugas konkuren ke cakupan leksikal, sehingga tugas tidak dapat hidup lebih lama daripada blok yang melahirkannya dan galat merambat alih-alih lenyap. Async/await memungkinkan Anda menulis kode konkuren yang terikat I/O dalam gaya berurutan. Masing-masing menaikkan lantai bagi penulis rata-rata, yang dibutuhkan tim besar.

Pahami model memori, atomisitas, dan visibilitas

Ketika Anda berbagi memori, dua sifat menggigit. Atomisitas berarti operasi terjadi sekaligus atau tidak sama sekali; kenaikan biasa (x = x + 1) tidak atomik, karena ia membaca, menambah, dan menulis sebagai tiga langkah yang dapat diinterupsi thread lain, begitulah penghitung kehilangan pembaruan. Visibilitas berarti bahwa tulisan satu thread dapat teramati oleh yang lain; tanpa sinkronisasi yang semestinya, nilai yang ditulis pada satu inti dapat mengendap di cache tak terlihat oleh inti lain, sehingga thread dapat berputar selamanya pada flag yang sudah diset. Model memori bahasa Anda mendefinisikan kapan tulisan menjadi terlihat dan urutan apa yang boleh disusun ulang kompiler dan CPU, jadi Anda tidak dapat mengasumsikan kode berjalan dalam urutan yang Anda tulis. Gunakan tipe atomik dan primitif sinkronisasi bahasa alih-alih menciptakan skema bebas-lock sendiri.

Gunakan primitif sinkronisasi dengan sengaja, dan rancang melawan deadlock

Ketika berbagi tak terhindarkan, raih primitif yang tepat dan hormati harganya. Lock atau mutex (mutual exclusion) memungkinkan satu thread sekali waktu memasuki bagian kritis, tetapi menserialkan akses, sehingga lock panas menjadi hambatan yang menghapus manfaat banyak inti. Semafor membatasi berapa banyak tugas yang boleh maju sekaligus, begitulah cara Anda membatasi kolam. Operasi atomik menawarkan pembaruan bebas-lock untuk nilai sederhana seperti penghitung, lebih murah daripada lock tetapi mudah disalahgunakan untuk apa pun yang majemuk. Lock membawa tiga mode kegagalan klasik. Deadlock adalah ketika tugas saling menunggu dalam siklus dan tak satu pun dapat maju, kasus buku teksnya dua thread yang masing-masing memegang satu lock dan menginginkan yang lain. Livelock adalah ketika tugas terus bereaksi satu sama lain tetapi tidak membuat kemajuan. Starvation adalah ketika tugas tidak pernah mendapat sumber daya karena yang lain terus menyerobot. Disiplin yang mencegahnya konkret: tetapkan urutan lock global, pegang lock sebentar, tambahkan timeout agar tugas yang macet gagal dengan lantang, jangan pernah memanggil kode tak dikenal sambil memegang lock, dan gunakan penjadwalan adil di mana starvation berisiko. Tuliskan aturan ini, karena penulis baru tidak dapat menemukannya kembali dari kode saja.

Batasi antrean, kolam, dan pekerjaan dalam proses dengan backpressure

Antrean tak terbatas adalah bom waktu. Di bawah lonjakan lalu lintas, pekerjaan tiba lebih cepat daripada yang terkuras, antrean tumbuh tanpa batas, memori penuh, dan layanan mati dengan cara yang tampak seperti crash kehabisan memori yang misterius alih-alih kelebihan beban yang sebenarnya. Batasi setiap antrean, plafon setiap kolam thread, dan terapkan backpressure: ketika sistem penuh, beri sinyal ke hulu untuk melambat atau tolak pekerjaan dengan cepat alih-alih menerima pekerjaan tak terbatas yang tidak dapat Anda selesaikan. Ukur kolam sesuai beban kerja (kira-kira jumlah inti untuk pekerjaan terikat CPU, lebih tinggi untuk pekerjaan terikat I/O di mana thread sebagian besar menunggu), dan perlakukan batas sebagai keputusan kapasitas yang disengaja. Ini terhubung dengan pola ketahanan bab 3.3.

Gunakan paralelisme data di tempat pekerjaan sangat mudah diparalelkan

Sebagian masalah terbagi dengan bersih: terapkan operasi yang sama pada setiap elemen set data besar, tanpa elemen yang bergantung pada yang lain. Paralelisme data ini jenis yang paling ramah, karena sedikit status bersama untuk diperebutkan dan percepatan dapat mendekati jumlah inti, seperti ditunjukkan pipeline map-reduce, operasi array paralel, dan kode numerik tervektorisasi. Bahkan di sini, hormati hukum Amdahl: langkah merge atau reduce sering serial dan membatasi keuntungan Anda, dan beban pemecahan dapat mendominasi untuk masukan kecil. Raihlah ketika pekerjaan per elemen substansial dan elemen benar-benar independen; jika tidak, versi berurutan paling sederhana sering cukup cepat sekaligus jauh lebih mudah dijaga benar, hal yang diperkuat praktik konstruksi bab 2.9.

Uji dan debug kode nondeterministik dengan sengaja

Bug konkurensi bersifat nondeterministik, jadi tes biasa, yang menjalankan satu interleaving, sebagian besar melewatkannya. Serang masalah dengan sengaja memakai tes stres dan fuzz yang menjalankan banyak tugas di bawah waktu acak untuk mengguncang urutan langka. Raih detektor race dan sanitiser thread, perkakas yang menginstrumentasi akses memori untuk menangkap race data bahkan ketika interleaving bermasalah tidak terjadi pada eksekusi ini. Di mana platform Anda menawarkannya, pakai simulasi deterministik atau penjadwal terkendali yang memutar ulang interleaving tertentu, mengubah heisenbug menjadi yang dapat direproduksi, dan rancang agar hang produksi memungkinkan Anda menangkap status thread dan kepemilikan lock, yang terkait dengan disiplin debugging bab 2.15. Di atas semuanya, pilih desain (imutabilitas, penyampaian pesan, kepemilikan tunggal) yang membuat seluruh kategori bug ini mustahil, karena bug yang tidak dapat Anda ciptakan adalah yang tidak pernah harus Anda debug.

Trade-off: kelebihan dan kekurangan

PendekatanKelebihanKekurangan
Memori bersama dengan lockCepat per operasi; akrabBug race, deadlock, dan visibilitas; sulit dijaga benar oleh banyak penulis
ImutabilitasTanpa sinkronisasi; pembacaan aman thread secara sepeleBiaya penyalinan; canggung untuk struktur mutabel besar
Penyampaian pesan (aktor, kanal)Aliran data eksplisit; seluruh kelas bug lenyapBeban per pesan; dapat menyembunyikan backpressure bila antrean tak terbatas
Async/awaitKonkurensi murah untuk pekerjaan terikat I/O; kode tampak berurutanTanpa paralelisme untuk kerja CPU; memblokir tugas menghentikan yang lain
Konkurensi terstrukturUmur tugas jelas; galat merambat; tanpa tugas bocorLebih baru, kurang tersedia di sebagian ekosistem
Paralelisme dataPercepatan nyaris linier pada pekerjaan independenLangit-langit Amdahl; beban mendominasi masukan kecil
Atomik / bebas-lockTanpa perebutan lock untuk nilai sederhanaSangat mudah keliru secara halus; sulit ditinjau

Ketegangan pusatnya adalah keamanan versus kecepatan mentah, dan penyelesaiannya adalah membeli kebenaran lebih dulu dan membelanjakan kinerja hanya di tempat pengukuran membuktikan Anda harus. Penguncian memori bersama mentah paling cepat per operasi dan paling berbahaya per baris kode; model tingkat lebih tinggi memakan sedikit throughput dan mengembalikan banyak keamanan dan kejelasan, dan untuk kode yang dipelihara banyak tangan pertukaran itu pasti sepadan. Sisakan konkurensi bebas-lock yang disetel tangan untuk titik panas kecil di mana profiler (bab 2.16) membuktikan beban koordinasi penting, dan simpan bahkan itu di balik batas yang teruji baik.

Pertanyaan untuk didiskusikan dengan tim Anda

  1. Untuk layanan tersibuk Anda, apakah beban kerjanya terikat I/O atau CPU, dan apakah desain konkurensi Anda cocok? Tim rutin menambah kolam thread ke layanan yang menghabiskan 95% waktunya menunggu basis data, memperoleh perebutan tetapi bukan throughput, atau mencoba memparalelkan komputasi yang bagian serialnya membatasi percepatan apa pun. Desain yang tepat mengikuti rezim: async atau kolam kecil untuk pekerjaan yang banyak menunggu, paralelisme nyata di seluruh inti untuk pekerjaan yang padat komputasi. Bawa profil yang menunjukkan ke mana waktu sebenarnya pergi, bukan asumsi, dan jika sebagian besar waktu dihabiskan menghitung, ukur bagian serial dan biarkan hukum Amdahl memberi tahu langit-langitnya. Jawabannya membentuk apakah Anda meraih async, kolam terbatas, atau paralelisme data.

  2. Apa bawaan tim Anda untuk berbagi status antartugas, dan apakah aman secara konstruksi? Pada tim besar, bawaan lebih penting daripada pengecualian, karena sebagian besar kode ditulis orang yang bukan spesialis konkurensi dan yang menyalin pola apa pun yang sudah ada. Jika bawaannya objek mutabel bersama yang dijaga lock ad hoc, Anda satu lock yang terlupa dari race yang muncul berbulan-bulan kemudian di produksi. Jika bawaannya imutabilitas dan penyampaian pesan, seluruh kategori bug tidak pernah terjadi, dan tempat langka yang benar-benar memerlukan memori bersama menonjol untuk tinjauan cermat. Diskusikan apa yang akan diraih insinyur baru hari ini, apakah tinjauan Anda akan menangkap tulisan tak tersinkronisasi, dan bagaimana menjadikan jalur aman sebagai yang mudah.

  3. Bagaimana Anda akan menemukan, mereproduksi, dan memperbaiki bug konkurensi yang muncul sekali dalam sejuta permintaan di produksi? Jawaban jujur bagi banyak tim adalah mereka tidak bisa, karena bug lenyap ketika mereka melihat dan tes mereka hanya menjalankan satu interleaving jinak. Itu seharusnya mengkhawatirkan Anda, karena bug ini merusak data secara diam-diam dan mengikis kepercayaan. Bicarakan apakah Anda menjalankan detektor race dan sanitiser thread di integrasi berkelanjutan, apakah Anda melakukan tes stres dengan waktu acak, dan apakah observabilitas produksi Anda menangkap status thread dan lock pada saat hang. Tim terbaik menjawab dengan membuat sebagian besar bug semacam itu mustahil lewat pilihan modelnya, sehingga sisa yang sedikit jarang dan terkandung.

  4. Di mana dalam sistem Anda antrean tak terbatas atau kolam thread tanpa plafon masih ada, dan apa yang terjadi padanya di bawah lonjakan sepuluh kali lipat mendadak? Ini penting karena pekerjaan dalam proses yang tak terbatas adalah kegagalan yang menyamar sebagai crash kehabisan memori yang misterius: pekerjaan tiba lebih cepat daripada yang terkuras, memori penuh, dan layanan mati tampak seperti kesalahan perangkat keras alih-alih kelebihan beban yang sebenarnya. Pertimbangan yang bersaing itu nyata, karena batas yang terlalu rendah menolak lalu lintas sah dan batas yang terlalu tinggi menunda crash alih-alih mencegahnya, jadi angkanya adalah keputusan kapasitas, bukan tebakan. Bawa inventaris setiap antrean dan kolam, batasnya saat ini (atau pengakuan bahwa tidak ada), perilaku backpressure saat penuh, dan bukti uji beban tentang bagaimana sistem merosot di tepi. Untuk armada enterprise satu antrean tak terbatas dapat berantai menjadi pemadaman seluruh armada, dan untuk platform pemerintah yang harus tetap tersedia bagi warga, penolakan anggun dengan galat jelas adalah kewajiban layanan, jadi batas dan jalur penolakannya termasuk dalam rencana kapasitas dan runbook, bukan dalam ingatan satu insinyur.

  5. Apa kebijakan tim Anda tentang memakai model konkurensi tingkat lebih tinggi versus lock tulisan tangan, dan di mana Anda mengizinkan pengecualian? Model bawaan menentukan seberapa aman perubahan rata-rata, karena sebagian besar penulis bukan spesialis konkurensi dan akan menyalin pola apa pun yang sudah ada: aktor, kanal, dan konkurensi terstruktur menaikkan lantai bagi semua orang, sementara penguncian mentah benar dalam teori dan sumber deadlock dalam praktik. Ketegangannya adalah model tingkat lebih tinggi memakan sedikit beban per pesan atau per tugas, dan profiler sesekali akan membuktikan jalur panas membutuhkan kode bebas-lock yang disetel tangan, sehingga larangan menyeluruh sama kelirunya dengan bebas segalanya. Bawa daftar tempat Anda telah turun di bawah bawaan aman, bukti pemrofilan yang membenarkan masing-masing, dan bagaimana setiap pengecualian dipagari di balik batas teruji dan urutan lock terdokumentasi. Di enterprise besar kebijakan ini yang menjaga ribuan kontributor agar tidak masing-masing menciptakan ulang skema tak aman, dan dalam sistem pemerintah berumur panjang kebijakan ini yang memungkinkan peninjau bertahun-tahun kemudian memahami mengapa pola berbahaya diizinkan dan mengonfirmasi masih dibenarkan.

  6. Ketika Anda memutuskan memparalelkan komputasi, bagaimana Anda mengukur bagian serial, dan siapa yang bertanggung jawab memastikan percepatan itu nyata? Tim rutin menyebarkan komputasi ke seluruh inti dan merayakan angka yang tidak akan pernah dikonfirmasi profiler, karena hukum Amdahl membatasi keuntungan pada kebalikan bagian serial berapa pun inti yang ditambah, dan beban pemecahan-dan-penggabungan dapat menghapus manfaat sepenuhnya untuk masukan kecil. Tarikan yang bersaing adalah paralelisme menambah kompleksitas nyata dan permukaan race baru, jadi pertanyaannya adalah apakah percepatan terukur membenarkan risiko kebenaran yang Anda ambil. Bawa profil yang mengisolasi bagian serial, ukuran masukan di mana paralelisme benar-benar menang, dan tolok ukur sebelum-dan-sesudah pada perangkat keras representatif alih-alih estimasi yang penuh harap. Untuk enterprise yang membayar armada komputasi besar, analisis bagian serial yang jujur berubah menjadi belanja perangkat keras yang dihemat atau disia-siakan, dan untuk badan pemerintah yang bertanggung jawab atas biaya sistem publik, orang yang menyetujui desain paralel harus dapat menunjukkan pengukuran yang membenarkannya di bawah audit.

Lensa sektor

Startup. Dengan tim mungil dan tanpa runway tersisa, belilah kebenaran dengan struktur, bukan dengan spesialis konkurensi yang tidak dapat Anda rekrut. Raih satu bawaan aman yang diberikan bahasa Anda, async/await untuk pekerjaan terikat I/O, satu tugas pemilik atau aktor untuk status bersama apa pun, dan lewati penguncian yang disetel tangan sepenuhnya. Race pembaruan hilang di jalur pembayaran dapat menenggelamkan Anda lebih cepat daripada fitur yang terlewat, jadi belanjakan sedikit kode tambahan untuk membuat kelas bug itu mustahil dan lanjutkan.

Bisnis kecil. Anda tidak punya orang yang tugasnya konkurensi, jadi pilih platform dan layanan terkelola yang menanganinya untuk Anda: transaksi basis data, antrean yang di-hosting, atau model permintaan kerangka kerja mengalahkan thread yang Anda pelihara dengan tangan. Ketika mengevaluasi perkakas, perlakukan “apakah ini menjadikan konkurensi aman secara bawaan” sebagai pertanyaan beli-versus-bangun, dan pilih opsi di mana interleaving yang salah tidak dapat diam-diam merusak catatan pelanggan. Jauhkan status mutabel bersama dari kode Anda sendiri di mana pun layanan terkelola terbatas dapat memegangnya.

Enterprise. Di banyak tim tujuannya adalah bawaan rumah yang menjaga ribuan kontributor tetap aman: imutabilitas dan penyampaian pesan sebagai norma, model tingkat lebih tinggi daripada lock mentah, antrean dan kolam terbatas dengan backpressure, dan urutan lock global terdokumentasi. Kodekan ini dalam standar rekayasa, tegakkan dengan detektor race dan tes stres di CI, dan atur pengecualian di mana profiler membenarkan kode bebas-lock agar masing-masing tetap di balik batas yang teruji dan ditinjau. Kelola kapasitas konkurensi sebagai perhatian seluruh armada dengan batas antrean dan ukuran kolam yang terikat pada beban terukur.

Pemerintah. Kebenaran dan kemampuan diaudit dalam sistem yang berjalan puluhan tahun mengalahkan throughput mentah. Tuntut agar setiap transisi status dicatat dan dapat diputar ulang, sehingga race yang dicurigai dapat direproduksi dan perbaikannya dibuktikan kepada badan pengawas, dan jaga jalur deterministik bebas-AI untuk keputusan yang menyentuh tunjangan, keselamatan, atau catatan publik. Pengadaan harus mewajibkan vendor mengungkap model konkurensi mereka dan bukti cakupan detektor race dan tes stres, karena jawaban keliru di bawah beban dalam sistem publik bukan ketidaknyamanan, melainkan sesuatu yang harus kelak dijelaskan oleh pejabat yang bertanggung jawab.

Contoh

Startup. Tim kecil merilis fitur pembayaran dan memperhatikan saldo akun kadang melayang beberapa sen di bawah beban. Penyebabnya adalah baca-ubah-tulis biasa pada bidang saldo dari penangan permintaan konkuren, race pembaruan hilang. Alih-alih menaburkan lock, mereka memindahkan saldo setiap akun di balik satu tugas pemilik yang memproses debit dan kredit sebagai pesan, satu per satu. Pelayangan lenyap, kode menjadi mudah dinalar, dan mereka menambahkan tes stres yang menembakkan ribuan transfer konkuren untuk menjaga perbaikan. Satu perubahan struktural, seluruh kelas bug dipensiunkan.

Enterprise. Layanan pesanan berthroughput tinggi yang menangani puluhan ribu permintaan per detik menderita lonjakan latensi periodik dan crash kehabisan memori sesekali selama lonjakan lalu lintas. Penyelidikan menemukan antrean kerja tak terbatas di balik kolam thread yang tumbuh tanpa batas begitu permintaan melebihi kapasitas. Tim membatasi antrean, memplafon kolam pada ukuran yang terikat pada jumlah inti, dan menambahkan backpressure yang menolak kelebihan beban dengan cepat dengan galat jelas. Throughput menjadi dapat diprediksi, crash berhenti, dan lock panas pada cache bersama diganti dengan struktur bebas-lock hanya setelah profiler membuktikan perebutan itu nyata. Bawaan aman bagi banyak penulis, konkurensi yang disetel hanya di tempat terukur.

Pemerintah. Platform tunjangan nasional berjalan puluhan tahun dan harus menghasilkan hasil yang dapat diaudit dan benar bahkan di bawah pembaruan kasus konkuren. Tim memilih imutabilitas dan penyampaian pesan sebagai bawaan rumah, membatasi setiap status mutabel pada satu pemilik, dan menetapkan urutan lock global di mana pun lock tersisa, semuanya dituliskan dalam standar rekayasa. Mereka menjalankan sanitiser thread dan tes stres acak di pipeline, dan merancang agar setiap transisi status dicatat dan dapat diputar ulang untuk pengawasan, yang memungkinkan mereka mereproduksi dan membuktikan perbaikan ketika interleaving langka dicurigai. Kebenaran dan kemampuan diaudit diperlakukan sebagai persyaratan kelas satu, bukan renungan kinerja.

Kasus bisnis: motivasi, ROI, dan TCO

Imbal hasil konkurensi yang disiplin tampak sebagai insiden yang tidak pernah terjadi. Satu race produksi dapat merusak data di ribuan catatan, dan biayanya mencakup jam rekayasa untuk menemukan bug yang bersembunyi saat diamati maupun biaya jauh lebih besar untuk merekonsiliasi data buruk, memberi tahu pengguna terdampak, dan membangun ulang kepercayaan. Ini termasuk cacat termahal untuk didiagnosis justru karena nondeterministik, sehingga mengejar satu heisenbug dapat melampaui upaya memilih model aman di muka.

Sisi positifnya juga tampak sebagai throughput dan biaya. Menyesuaikan konkurensi memungkinkan layanan menangani beban jauh lebih banyak pada perangkat keras yang sama, penghematan berulang untuk armada besar, sementara backpressure dan antrean terbatas mencegah pemadaman berantai yang mengubah lonjakan lalu lintas menjadi insiden publik. Total biaya kepemilikannya sederhana dan sebagian besar budaya: Anda berinvestasi pada gaya rumah (imutabilitas, penyampaian pesan, konkurensi terstruktur), pada perkakas (detektor race, sanitiser thread, harness stres di CI), dan pada standar yang mengkodekan urutan lock dan pembatasan. Alternatifnya adalah basis kode di mana kebenaran bergantung pada setiap penulis menjadi pakar selamanya, yang tidak dapat dipertahankan tim yang tumbuh mana pun. Ajukan kasus kepada pimpinan dalam satuan mereka: terjemahkan race yang dicegah menjadi insiden kerusakan data yang dihindari, backpressure menjadi pemadaman yang dicegah, dan bawaan aman menjadi waktu orientasi yang dihemat.

Anti-pola dan jebakan

  • Menambah thread demi kecepatan pada pekerjaan terikat I/O. Lebih banyak thread pada layanan yang banyak menunggu membeli perebutan, bukan throughput.
  • Status mutabel bersama di mana-mana. Thread mana pun yang mengubah objek mana pun menjadikan kebenaran soal keberuntungan yang tidak dapat diverifikasi peninjau.
  • Antrean dan kolam tak terbatas. Lonjakan menumbuhkan antrean sampai memori mati; crash tampak misterius tetapi kelebihan beban biasa.
  • Penguncian ad hoc tanpa urutan global. Lock yang diambil dalam urutan berbeda di seluruh basis kode deadlock di bawah beban.
  • Mengasumsikan kode berjalan dalam urutan tertulis. Mengabaikan model memori, sehingga bug visibilitas meninggalkan thread berputar pada nilai basi.
  • Kecerdikan bebas-lock buatan sendiri. Skema bebas-lock kustom hampir selalu keliru secara halus dan nyaris mustahil ditinjau.
  • Menguji hanya interleaving bahagia. Tes deterministik lolos sementara urutan satu-dalam-sejuta merusak produksi.
  • Memanggil kode tak dikenal sambil memegang lock. Callback yang memblokir atau masuk kembali mengubah bagian kritis menjadi deadlock.

Model kematangan

  • Tingkat 1, Memulai: Konkurensi ad hoc dan reaktif. Thread dan lock ditambahkan berdasarkan naluri, status mutabel bersama ada di mana-mana, dan antrean tak terbatas. Race condition muncul sebagai insiden produksi tak dapat direproduksi yang tidak dapat didiagnosis siapa pun, dan tidak ada perkakas untuk menangkapnya.
  • Tingkat 2, Mengembangkan: Beberapa tim telah mempelajari praktik dasar: mereka memakai lock dengan lebih hati-hati dan membatasi antrean paling jelas mereka. Ada kesadaran informal tentang race dan deadlock, dan beberapa jalur kritis mendapat pengawasan ekstra. Praktik tidak konsisten antartim, pengujian masih sebagian besar satu-interleaving, dan pola aman hidup pada individu alih-alih tertulis.
  • Tingkat 3, Membakukan: Organisasi memiliki gaya rumah terdokumentasi yang ditegakkan di seluruh organisasi: imutabilitas dan penyampaian pesan sebagai bawaan, model tingkat lebih tinggi daripada lock mentah, antrean dan kolam terbatas dengan backpressure, dan urutan lock global terdokumentasi. Detektor race dan tes stres berjalan di CI, dan pilihan konkurensi mengikuti apakah pekerjaan terikat I/O atau CPU.
  • Tingkat 4, Mengelola: Organisasi mengukur dan mengendalikan postur konkurensinya terhadap garis dasar. Ia melacak cakupan detektor race dan sanitiser thread di seluruh layanan, mencatat kedalaman antrean, waktu tunggu lock, kejenuhan kolam, dan tingkat penolakan sebagai metrik terpantau, dan menguji beban kurva degradasi sehingga setiap batas adalah keputusan kapasitas berbasis data. Insiden konkurensi dihitung dan ditren, bagian serial beban kerja yang diparalelkan diukur terhadap percepatan yang benar-benar dicapai, dan keputusan jalan atau tidak pada desain baru bertumpu pada bukti itu alih-alih naluri.
  • Tingkat 5, Mengorkestrasi: Konkurensi yang aman adalah jalur dengan hambatan terkecil bagi setiap penulis, dan praktiknya terus diperbaiki dan terintegrasi di seluruh organisasi. Seluruh kelas bug mustahil secara konstruksi, titik panas disetel hanya di tempat pemrofilan membuktikan perlunya, dan pemutaran ulang deterministik membuat bug sisa yang langka dapat direproduksi. Kebenaran dan kemampuan diaudit adalah sifat yang dibela terus-menerus, batas kapasitas beradaptasi dengan beban teramati, dan standar berkembang seiring bergesernya platform dan beban kerja.

Gagasan untuk didiskusikan

  1. Jika Anda mengaudit layanan tersibuk Anda hari ini, berapa banyak statusnya yang bersama dan mutabel, dan berapa banyak pembagian itu yang benar-benar perlu?
  2. Apa jawaban bawaan tim Anda ketika seseorang membutuhkan dua tugas untuk berkoordinasi, dan apakah Anda lebih suka imutabilitas atau penyampaian pesan?
  3. Di mana antrean tak terbatas atau kolam tanpa plafon masih bersembunyi dalam sistem Anda, dan apa yang akan terjadi padanya di bawah lonjakan lalu lintas sepuluh kali lipat mendadak?
  4. Apakah eksekusi integrasi berkelanjutan Anda mencakup detektor race atau sanitiser thread, dan kapan terakhir ada yang menangkap sesuatu sebelum produksi?
  5. Untuk beban kerja Anda yang paling diparalelkan, berapa bagian serialnya, dan apakah hukum Amdahl membatasi percepatan yang sebenarnya Anda kejar?
  6. Dapatkah tim Anda mereproduksi bug interleaving satu-dalam-sejuta sesuai permintaan, dan apa yang dibutuhkan untuk sampai ke sana?

Poin-poin utama

  • Konkurensi menstrukturkan program sebagai tugas independen; paralelisme mengeksekusinya sekaligus. Putuskan mana yang Anda butuhkan sebelum menambah thread.
  • Status mutabel bersama adalah akar hampir setiap bug konkurensi; pilih imutabilitas dan penyampaian pesan sebagai bawaan aman bagi banyak penulis.
  • Raih model tingkat lebih tinggi (aktor, kanal, konkurensi terstruktur, async/await) sebelum lock tulisan tangan, yang benar dalam teori dan berbahaya dalam praktik.
  • Pahami atomisitas, visibilitas, dan model memori Anda; pakai primitif yang tepat, pegang lock sebentar, dan tetapkan urutan lock global untuk menghindari deadlock, livelock, dan starvation.
  • Batasi setiap antrean dan kolam serta terapkan backpressure, agar lonjakan merosot dengan anggun alih-alih crash (bab 3.3).
  • Uji interleaving dengan sengaja memakai detektor race, tes stres, dan pemutaran ulang (bab 2.15), dan hormati hukum Amdahl saat memparalelkan (bab 2.16).
  • Bagi enterprise ini throughput dan insiden yang dicegah; bagi pemerintah ini kebenaran dan kemampuan diaudit dalam sistem berumur panjang.

Referensi dan bacaan lanjutan

  • Brian Goetz et al., Java Concurrency in Practice (atomisitas, visibilitas, model memori, dan publikasi aman).
  • Herb Sutter, “The Free Lunch Is Over” (mengapa perangkat lunak harus merangkul konkurensi saat kecepatan clock mendatar).
  • Leslie Lamport, “Time, Clocks, and the Ordering of Events in a Distributed System” (pengurutan dan fondasi penalaran konkuren).
  • C. A. R. Hoare, “Communicating Sequential Processes” (Communications of the ACM, 1978): model CSP di balik kanal.
  • Carl Hewitt, Peter Bishop, dan Richard Steiger, “A Universal Modular Actor Formalism for Artificial Intelligence” (asal model aktor).
  • Edsger W. Dijkstra, “Cooperating Sequential Processes” (semafor, pengecualian bersama, dan masalah deadlock).
  • Maurice Herlihy dan Nir Shavit, The Art of Multiprocessor Programming (lock, atomik, dan struktur data bebas-lock).
  • Nathaniel J. Smith, “Notes on Structured Concurrency, or: Go Statement Considered Harmful” (argumen untuk konkurensi terstruktur).
  • Martin Kleppmann, Designing Data-Intensive Applications (konkurensi dan konsistensi di mana memori bertemu sistem terdistribusi).
  • Gene M. Amdahl, “Validity of the Single Processor Approach to Achieving Large-Scale Computing Capabilities” (1967): asal hukum Amdahl.