Prinsip dan Pola Desain untuk Aplikasi yang Sangat Bersamaan

1. Ikhtisar

Dalam tutorial ini, kita akan membahas beberapa prinsip dan pola desain yang telah ditetapkan dari waktu ke waktu untuk membangun aplikasi yang sangat bersamaan.

Namun, perlu dicatat bahwa mendesain aplikasi bersamaan adalah topik yang luas dan kompleks, dan karenanya tidak ada tutorial yang dapat mengklaim perawatannya secara lengkap. Yang akan kami bahas di sini adalah beberapa trik populer yang sering digunakan!

2. Dasar-dasar Concurrency

Sebelum melangkah lebih jauh, mari luangkan waktu untuk memahami dasar-dasarnya. Untuk memulainya, kita harus memperjelas pemahaman kita tentang apa yang kita sebut program bersamaan. Kami merujuk ke program yang bersamaan jika beberapa komputasi terjadi pada waktu yang sama .

Sekarang, perhatikan bahwa kami telah menyebutkan penghitungan yang terjadi pada saat yang sama - yaitu, sedang berlangsung pada waktu yang sama. Namun, mereka mungkin atau mungkin tidak dijalankan secara bersamaan. Penting untuk memahami perbedaannya karena menjalankan komputasi secara bersamaan disebut paralel .

2.1. Bagaimana Cara Membuat Modul Bersamaan?

Penting untuk memahami bagaimana kita dapat membuat modul bersamaan. Ada banyak opsi, tetapi kami akan fokus pada dua pilihan populer di sini:

  • Proses : Proses adalah turunan dari program yang sedang berjalan yang diisolasi dari proses lain di mesin yang sama. Setiap proses pada mesin memiliki ruang dan waktu tersendiri. Oleh karena itu, biasanya tidak mungkin untuk berbagi memori antar proses, dan mereka harus berkomunikasi dengan meneruskan pesan.
  • Thread : Sebuah thread, di sisi lain, hanyalah sebuah segmen dari sebuah proses . Mungkin ada beberapa utas dalam program yang berbagi ruang memori yang sama. Namun, setiap utas memiliki tumpukan dan prioritas yang unik. Sebuah utas bisa asli (dijadwalkan secara asli oleh sistem operasi) atau hijau (dijadwalkan oleh perpustakaan runtime).

2.2. Bagaimana Modul Bersamaan Berinteraksi?

Ini cukup ideal jika modul konkuren tidak harus berkomunikasi, tetapi seringkali tidak demikian. Ini memunculkan dua model pemrograman bersamaan:

  • Memori Bersama : Dalam model ini, modul bersamaan berinteraksi dengan membaca dan menulis objek bersama di memori . Hal ini sering menyebabkan interleaving dari penghitungan serentak, yang menyebabkan kondisi balapan. Oleh karena itu, secara non-deterministik dapat menyebabkan keadaan yang salah.
  • Message Passing : Dalam model ini, modul bersamaan berinteraksi dengan meneruskan pesan satu sama lain melalui saluran komunikasi . Di sini, setiap modul memproses pesan masuk secara berurutan. Karena tidak ada status bersama, program ini relatif lebih mudah, tetapi ini masih tidak lepas dari ketentuan balapan!

2.3. Bagaimana Modul Konkuren Dieksekusi?

Sudah lama sejak Hukum Moore membentur tembok sehubungan dengan kecepatan clock prosesor. Sebaliknya, karena kita harus berkembang, kita telah mulai mengemas banyak prosesor ke dalam chip yang sama, yang sering disebut prosesor multicore. Tapi tetap saja, tidak umum mendengar tentang prosesor yang memiliki lebih dari 32 inti.

Sekarang, kita tahu bahwa satu inti hanya dapat mengeksekusi satu utas, atau serangkaian instruksi, pada satu waktu. Namun, jumlah proses dan utas masing-masing bisa ratusan dan ribuan. Jadi, bagaimana cara kerjanya? Di sinilah sistem operasi mensimulasikan konkurensi untuk kami . Sistem operasi mencapai ini dengan pemotongan waktu - yang secara efektif berarti bahwa prosesor sering beralih di antara utas, tidak dapat diprediksi, dan tidak ditentukan.

3. Masalah dalam Pemrograman Bersamaan

Saat kita membahas tentang prinsip dan pola untuk merancang aplikasi bersamaan, akan lebih bijaksana untuk terlebih dahulu memahami apa masalah tipikal itu.

Untuk sebagian besar, pengalaman kami dengan pemrograman bersamaan melibatkan penggunaan utas asli dengan memori bersama . Karenanya, kami akan fokus pada beberapa masalah umum yang muncul darinya:

  • Mutual Exclusion (Synchronization Primitives) : Untaian interleaving harus memiliki akses eksklusif ke status atau memori bersama untuk memastikan kebenaran program . Sinkronisasi sumber daya bersama adalah metode populer untuk mencapai pengecualian bersama. Ada beberapa primitif sinkronisasi yang tersedia untuk digunakan - misalnya, kunci, monitor, semaphore, atau mutex. Namun, pemrograman untuk pengecualian bersama rentan terhadap kesalahan dan sering kali dapat menyebabkan kemacetan kinerja. Ada beberapa masalah yang dibahas dengan baik terkait dengan ini seperti deadlock dan livelock.
  • Pengalihan Konteks (Benang Kelas Berat) : Setiap sistem operasi memiliki dukungan asli, meskipun bervariasi, untuk modul bersamaan seperti proses dan utas. Seperti yang telah dibahas, salah satu layanan dasar yang disediakan sistem operasi adalah penjadwalan utas untuk dieksekusi pada sejumlah prosesor melalui pemotongan waktu. Sekarang, ini secara efektif berarti bahwa utas sering beralih di antara status yang berbeda . Dalam prosesnya, status mereka saat ini perlu disimpan dan dilanjutkan. Ini adalah aktivitas yang memakan waktu yang berdampak langsung pada keseluruhan throughput.

4. Pola Desain untuk Konkurensi Tinggi

Sekarang, setelah kita memahami dasar-dasar pemrograman bersamaan dan masalah umum di dalamnya, sekarang saatnya memahami beberapa pola umum untuk menghindari masalah ini. Kita harus menegaskan kembali bahwa pemrograman bersamaan adalah tugas sulit yang membutuhkan banyak pengalaman. Karenanya, mengikuti beberapa pola yang ditetapkan dapat membuat tugas lebih mudah.

4.1. Konkurensi Berbasis Aktor

Desain pertama yang akan kita diskusikan berkenaan dengan pemrograman konkuren disebut Model Aktor. Ini adalah model matematika dari komputasi bersamaan yang pada dasarnya memperlakukan segala sesuatu sebagai aktor . Aktor dapat menyampaikan pesan satu sama lain dan, sebagai tanggapan atas pesan, dapat membuat keputusan lokal. Ini pertama kali diusulkan oleh Carl Hewitt dan telah menginspirasi sejumlah bahasa pemrograman.

Konstruksi utama Scala untuk pemrograman bersamaan adalah aktor. Aktor adalah objek normal di Scala yang dapat kita buat dengan membuat instance kelas Actor . Selain itu, pustaka Scala Actors menyediakan banyak operasi aktor yang berguna:

class myActor extends Actor { def act() { while(true) { receive { // Perform some action } } } }

Dalam contoh di atas, panggilan ke metode terima di dalam loop tak terbatas menangguhkan aktor hingga sebuah pesan tiba. Setelah tiba, pesan dihapus dari kotak surat aktor, dan tindakan yang diperlukan diambil.

Model aktor menghilangkan salah satu masalah mendasar dengan pemrograman bersamaan - memori bersama . Aktor berkomunikasi melalui pesan, dan setiap aktor memproses pesan dari kotak surat eksklusifnya secara berurutan. Namun, kami mengeksekusi aktor melalui kumpulan utas. Dan kami telah melihat bahwa utas asli dapat menjadi kelas berat dan, karenanya, jumlahnya terbatas.

Tentu saja ada pola lain yang dapat membantu kita di sini - kita akan membahasnya nanti!

4.2. Konkurensi Berbasis Peristiwa

Desain berbasis peristiwa secara eksplisit mengatasi masalah bahwa utas asli mahal untuk ditelurkan dan dioperasikan. Salah satu desain berbasis event adalah event loop. Perulangan peristiwa bekerja dengan penyedia acara dan satu set penangan peristiwa. Dalam penyiapan ini, loop peristiwa memblokir pada penyedia acara dan mengirimkan peristiwa ke pengendali peristiwa pada saat kedatangan .

Pada dasarnya, event loop hanyalah sebuah event dispatcher! Perulangan peristiwa itu sendiri dapat berjalan hanya di satu utas asli. Jadi, apa yang sebenarnya terjadi dalam sebuah event loop? Mari kita lihat pseudo-code dari sebuah event loop yang sangat sederhana sebagai contoh:

while(true) { events = getEvents(); for(e in events) processEvent(e); }

Pada dasarnya, semua event loop yang kita lakukan adalah terus mencari event dan, ketika event ditemukan, memprosesnya. Pendekatannya sangat sederhana, tetapi menuai keuntungan dari desain yang digerakkan oleh peristiwa.

Membangun aplikasi bersamaan menggunakan desain ini memberi kontrol lebih pada aplikasi. Selain itu, ini menghilangkan beberapa masalah khas dari aplikasi multi-utas - misalnya, kebuntuan.

JavaScript mengimplementasikan event loop untuk menawarkan pemrograman asynchronous . Ini memelihara tumpukan panggilan untuk melacak semua fungsi yang akan dijalankan. Ini juga memelihara antrian acara untuk mengirim fungsi baru untuk diproses. Perulangan peristiwa secara konstan memeriksa tumpukan panggilan dan menambahkan fungsi baru dari antrean peristiwa. Semua panggilan asinkron dikirim ke API web, biasanya disediakan oleh browser.

Perulangan peristiwa itu sendiri dapat dijalankan dari satu utas, tetapi API web menyediakan utas terpisah.

4.3. Algoritma Non-Pemblokiran

Dalam algoritme non-pemblokiran, penangguhan satu utas tidak menyebabkan penangguhan utas lainnya. Kami telah melihat bahwa kami hanya dapat memiliki sejumlah kecil utas asli dalam aplikasi kami. Sekarang, algoritme yang memblokir sebuah utas jelas menurunkan throughput secara signifikan dan mencegah kita membangun aplikasi yang sangat serentak.

Algoritme non-pemblokiran selalu menggunakan primitif atom perbandingan-dan-tukar yang disediakan oleh perangkat keras yang mendasarinya . Ini berarti bahwa perangkat keras akan membandingkan konten lokasi memori dengan nilai yang diberikan, dan hanya jika nilainya sama, perangkat keras akan memperbarui nilai ke nilai yang baru. Ini mungkin terlihat sederhana, tetapi secara efektif memberi kita operasi atom yang jika tidak memerlukan sinkronisasi.

Ini berarti bahwa kita harus menulis struktur dan pustaka data baru yang menggunakan operasi atom ini. Ini telah memberi kami serangkaian besar implementasi bebas menunggu dan bebas kunci dalam beberapa bahasa. Java memiliki beberapa struktur data non-pemblokiran seperti AtomicBoolean , AtomicInteger , AtomicLong , dan AtomicReference .

Pertimbangkan aplikasi di mana beberapa utas mencoba mengakses kode yang sama:

boolean open = false; if(!open) { // Do Something open=false; }

Jelas, kode di atas tidak aman untuk thread, dan perilakunya di lingkungan multi-threaded tidak dapat diprediksi. Opsi kami di sini adalah untuk menyinkronkan bagian kode ini dengan kunci atau menggunakan operasi atom:

AtomicBoolean open = new AtomicBoolean(false); if(open.compareAndSet(false, true) { // Do Something }

Seperti yang bisa kita lihat, menggunakan struktur data non-pemblokiran seperti AtomicBoolean membantu kita menulis kode yang aman untuk thread tanpa mengganggu kekurangan dari kunci!

5. Dukungan dalam Bahasa Pemrograman

Kami telah melihat bahwa ada banyak cara untuk membuat modul bersamaan. Meskipun bahasa pemrograman memang membuat perbedaan, sebagian besar adalah bagaimana sistem operasi yang mendasarinya mendukung konsep tersebut. Namun, karena konkurensi berbasis utas yang didukung oleh utas asli mencapai dinding baru sehubungan dengan skalabilitas, kami selalu membutuhkan opsi baru.

Menerapkan beberapa praktik desain yang kita diskusikan di bagian terakhir terbukti efektif. Namun, kita harus ingat bahwa itu memang memperumit pemrograman. Apa yang benar-benar kita butuhkan adalah sesuatu yang memberikan kekuatan konkurensi berbasis thread tanpa efek yang tidak diinginkan yang ditimbulkannya.

Salah satu solusi yang tersedia bagi kami adalah benang hijau. Benang hijau adalah utas yang dijadwalkan oleh pustaka runtime alih-alih dijadwalkan secara asli oleh sistem operasi yang mendasarinya. Meskipun ini tidak menghilangkan semua masalah dalam konkurensi berbasis utas, ini pasti dapat memberi kami kinerja yang lebih baik dalam beberapa kasus.

Sekarang, tidak sepele untuk menggunakan benang hijau kecuali bahasa pemrograman yang kami pilih untuk digunakan mendukungnya. Tidak semua bahasa pemrograman memiliki dukungan bawaan ini. Juga, apa yang secara longgar kita sebut sebagai benang hijau dapat diimplementasikan dengan cara yang sangat unik oleh bahasa pemrograman yang berbeda. Mari kita lihat beberapa dari opsi ini yang tersedia untuk kita.

5.1. Goroutine di Go

Goroutine dalam bahasa pemrograman Go adalah utas yang ringan. Mereka menawarkan fungsi atau metode yang dapat berjalan bersamaan dengan fungsi atau metode lain. Goroutine sangat murah karena mereka hanya menempati beberapa kilobyte dalam ukuran tumpukan, untuk memulai .

Yang terpenting, goroutine dibuat multipleks dengan jumlah utas asli yang lebih sedikit. Selain itu, goroutine berkomunikasi satu sama lain menggunakan saluran, sehingga menghindari akses ke memori bersama. Kami mendapatkan hampir semua yang kami butuhkan, dan coba tebak - tanpa melakukan apa pun!

5.2. Proses di Erlang

Di Erlang, setiap rangkaian eksekusi disebut proses. Tapi, itu tidak seperti proses yang telah kita diskusikan sejauh ini! Proses Erlang ringan dengan footprint memori kecil dan cepat dibuat dan dibuang dengan overhead penjadwalan rendah.

Di bawah tenda, proses Erlang tidak lain adalah fungsi yang waktu prosesnya menangani penjadwalan. Selain itu, proses Erlang tidak membagikan data apa pun, dan mereka berkomunikasi satu sama lain melalui penyampaian pesan. Inilah alasan mengapa kami menyebut "proses" ini di tempat pertama!

5.3. Serat di Jawa (Proposal)

Kisah konkurensi dengan Java telah menjadi evolusi yang berkelanjutan. Java memang memiliki dukungan untuk benang hijau, setidaknya untuk sistem operasi Solaris, untuk memulai. Namun, ini dihentikan karena rintangan di luar cakupan tutorial ini.

Sejak itu, konkurensi di Java adalah tentang utas asli dan cara bekerja dengannya dengan cerdas! Namun karena alasan yang jelas, kami mungkin akan segera memiliki abstraksi konkurensi baru di Java, yang disebut fiber. Project Loom mengusulkan untuk memperkenalkan kelanjutan bersama dengan serat, yang dapat mengubah cara kami menulis aplikasi bersamaan di Java!

Ini hanyalah sekilas tentang apa yang tersedia dalam bahasa pemrograman yang berbeda. Ada cara yang jauh lebih menarik yang telah dicoba oleh bahasa pemrograman lain untuk menangani konkurensi.

Selain itu, perlu dicatat bahwa kombinasi pola desain yang dibahas di bagian terakhir, bersama dengan dukungan bahasa pemrograman untuk abstraksi seperti benang hijau, bisa sangat berguna saat merancang aplikasi yang sangat serentak.

6. Aplikasi Konkurensi Tinggi

Aplikasi dunia nyata sering kali memiliki banyak komponen yang berinteraksi satu sama lain melalui kabel. Kami biasanya mengaksesnya melalui internet, dan ini terdiri dari beberapa layanan seperti layanan proxy, gateway, layanan web, database, layanan direktori, dan sistem file.

Bagaimana kita memastikan konkurensi tinggi dalam situasi seperti itu? Mari jelajahi beberapa lapisan ini dan opsi yang kita miliki untuk membangun aplikasi yang sangat serentak.

Seperti yang telah kita lihat di bagian sebelumnya, kunci untuk membangun aplikasi konkurensi tinggi adalah menggunakan beberapa konsep desain yang dibahas di sana. Kita perlu memilih perangkat lunak yang tepat untuk pekerjaan itu - yang sudah menerapkan beberapa praktik ini.

6.1. Lapisan Web

Web biasanya merupakan lapisan pertama tempat permintaan pengguna tiba, dan penyediaan untuk konkurensi tinggi tidak bisa dihindari di sini. Mari kita lihat apa saja dari opsinya:

  • Node (juga disebut NodeJS atau Node.js) adalah runtime JavaScript lintas platform sumber terbuka yang dibangun di mesin JavaScript V8 Chrome. Node bekerja cukup baik dalam menangani operasi I / O asynchronous. Alasan Node melakukannya dengan sangat baik adalah karena Node mengimplementasikan loop peristiwa pada satu thread. Perulangan peristiwa dengan bantuan callback menangani semua operasi pemblokiran seperti I / O secara asinkron.
  • nginx adalah server web sumber terbuka yang biasa kami gunakan sebagai proxy terbalik di antara penggunaan lainnya. Alasan nginx menyediakan konkurensi tinggi adalah karena menggunakan pendekatan asynchronous, event-driven. nginx beroperasi dengan proses master dalam satu utas. Proses master mempertahankan proses pekerja yang melakukan pemrosesan sebenarnya. Karenanya, pekerja memproses setiap permintaan secara bersamaan.

6.2. Lapisan Aplikasi

Saat mendesain aplikasi, ada beberapa alat untuk membantu kami membangun konkurensi tinggi. Mari kita periksa beberapa pustaka dan kerangka kerja berikut yang tersedia untuk kita:

  • Akka adalah toolkit yang ditulis dalam Scala untuk membangun aplikasi yang sangat serentak dan terdistribusi di JVM. Pendekatan Akka dalam menangani konkurensi didasarkan pada model aktor yang telah kita diskusikan sebelumnya. Akka menciptakan lapisan antara aktor dan sistem yang mendasarinya. Kerangka kerja ini menangani kerumitan dalam membuat dan menjadwalkan utas, menerima dan mengirim pesan.
  • Project Reactor adalah pustaka reaktif untuk membangun aplikasi non-pemblokiran di JVM. Ini didasarkan pada spesifikasi Reactive Streams dan berfokus pada penyampaian pesan yang efisien dan manajemen permintaan (tekanan balik). Operator reaktor dan penjadwal dapat mempertahankan tingkat throughput yang tinggi untuk pesan. Beberapa framework populer menyediakan implementasi reaktor, termasuk Spring WebFlux dan RSocket.
  • Netty adalah kerangka kerja aplikasi jaringan asynchronous, event-driven. Kami dapat menggunakan Netty untuk mengembangkan klien dan server protokol yang sangat bersamaan. Netty memanfaatkan NIO, yang merupakan kumpulan API Java yang menawarkan transfer data asinkron melalui buffer dan saluran. Ini memberi kita beberapa keuntungan seperti throughput yang lebih baik, latensi lebih rendah, konsumsi sumber daya lebih sedikit, dan meminimalkan salinan memori yang tidak perlu.

6.3. Lapisan Data

Terakhir, tidak ada aplikasi yang lengkap tanpa datanya, dan data berasal dari penyimpanan persisten. Saat kita membahas konkurensi tinggi sehubungan dengan database, sebagian besar fokus tetap pada keluarga NoSQL. Ini terutama karena skalabilitas linier yang ditawarkan database NoSQL tetapi sulit dicapai dalam varian relasional. Mari kita lihat dua alat populer untuk lapisan data:

  • Cassandra adalah database terdistribusi NoSQL gratis dan open-source yang menyediakan ketersediaan tinggi, skalabilitas tinggi, dan toleransi kesalahan pada perangkat keras komoditas. Namun, Cassandra tidak menyediakan transaksi ACID yang mencakup banyak tabel. Jadi jika aplikasi kita tidak membutuhkan konsistensi dan transaksi yang kuat, kita bisa mendapatkan keuntungan dari operasi latensi rendah Cassandra.
  • Kafka adalah platform streaming terdistribusi . Kafka menyimpan aliran rekaman dalam kategori yang disebut topik. Ini dapat memberikan skalabilitas horizontal linier untuk produsen dan konsumen rekaman sementara, pada saat yang sama, memberikan keandalan dan daya tahan yang tinggi. Partisi, replika, dan broker adalah beberapa konsep dasar yang menyediakan konkurensi yang didistribusikan secara masif.

6.4. Lapisan Cache

Tidak ada aplikasi web di dunia modern yang bertujuan untuk konkurensi tinggi yang mampu mencapai database setiap saat. Itu membuat kita memilih cache - lebih disukai cache dalam memori yang dapat mendukung aplikasi kita yang sangat bersamaan:

  • Hazelcast adalah penyimpanan objek dalam memori dan mesin komputasi terdistribusi, ramah-cloud, yang mendukung berbagai macam struktur data seperti Map , Set , List , MultiMap , RingBuffer , dan HyperLogLog . Ini memiliki replikasi built-in dan menawarkan ketersediaan tinggi dan partisi otomatis.
  • Redis adalah penyimpanan struktur data dalam memori yang terutama kami gunakan sebagai cache . Ini menyediakan database nilai kunci dalam memori dengan daya tahan opsional. Struktur data yang didukung mencakup string, hash, daftar, dan set. Redis memiliki replikasi bawaan dan menawarkan ketersediaan tinggi dan partisi otomatis. Jika kami tidak membutuhkan ketekunan, Redis dapat menawarkan kepada kami cache dalam memori yang kaya fitur, berjejaring, dengan kinerja luar biasa.

Tentu saja, kami hampir tidak menyentuh permukaan dari apa yang tersedia bagi kami dalam upaya kami untuk membangun aplikasi yang sangat serentak. Penting untuk dicatat bahwa, lebih dari perangkat lunak yang tersedia, persyaratan kami harus memandu kami untuk membuat desain yang sesuai. Beberapa dari opsi ini mungkin cocok, sementara yang lain mungkin tidak sesuai.

Dan, jangan lupa bahwa ada lebih banyak opsi yang tersedia yang mungkin lebih sesuai untuk kebutuhan kita.

7. Kesimpulan

Pada artikel ini, kita membahas dasar-dasar pemrograman konkuren. Kami memahami beberapa aspek fundamental dari konkurensi dan masalah yang dapat ditimbulkannya. Selanjutnya, kami membahas beberapa pola desain yang dapat membantu kami menghindari masalah khas dalam pemrograman bersamaan.

Terakhir, kami membahas beberapa kerangka kerja, pustaka, dan perangkat lunak yang tersedia bagi kami untuk membangun aplikasi ujung ke ujung yang sangat serentak.