7 days of WordPress plugins, themes & templates - for free!* Unlimited asset downloads! Start 7-Day Free Trial
Advertisement
  1. Code
  2. Elixir

Apa itu GenServer, dan Mengapa Anda Harus Peduli?

Scroll to top
Read Time: 14 mins

Indonesian (Bahasa Indonesia) translation by Febri Ardian Sanjoyo (you can also view the original English article)

Pada artikel ini Anda akan mempelajari dasar-dasar concurrency di Elixir dan melihat bagaimana proses spawn, mengirim dan menerima pesan, dan menciptakan proses yang berjalan lama. Anda juga akan belajar tentang GenServer, lihat bagaimana penggunaannya di aplikasi Anda, dan temukan beberapa barang yang disediakan untuk Anda.

Seperti yang mungkin Anda ketahui, Elixir adalah bahasa fungsional yang digunakan untuk membangun sistem fault-tolerant dan simultan yang menangani banyak permintaan simultan. BEAM (Erlang virtual machine) menggunakan proses untuk melakukan berbagai tugas secara bersamaan, yang berarti, misalnya, melayani satu permintaan tidak menghalangi yang lain. Prosesnya ringan dan terisolasi, yang berarti tidak ada memori dan bahkan jika satu proses mogok, yang lain dapat terus berjalan.

Proses BEAM sangat berbeda dengan proses OS. Pada dasarnya, BEAM berjalan dalam satu proses OS dan menggunakan penjadwal sendiri. Setiap scheduler menempati satu CPU core, berjalan di thread terpisah, dan mungkin menangani ribuan proses secara simultan (yang bergiliran untuk dijalankan). Anda bisa membaca sedikit lebih banyak tentang BEAM dan multithreading di StackOverflow.

Jadi, seperti yang Anda lihat, proses BEAM (saya akan mengatakan hanya "proses" mulai sekarang) sangat penting dalam Elixir. Bahasa ini memberi Anda beberapa alat tingkat rendah untuk proses spawn secara manual, mempertahankan status, dan menangani permintaan. Namun, hanya sedikit orang yang menggunakannya - lebih umum mengandalkan kerangka Open Telecom Platform (OTP) untuk melakukan itu.

OTP saat ini tidak ada hubungannya dengan telepon, ini adalah kerangka kerja umum untuk membangun sistem bersamaan yang kompleks. Ini mendefinisikan bagaimana aplikasi Anda harus terstruktur dan menyediakan database serta banyak alat yang sangat berguna untuk menciptakan proses server, pulih dari kesalahan, melakukan pencatatan, dll. Pada artikel ini, kita akan membahas tentang perilaku server yang disebut GenServer yang disediakan oleh OTP.

Anda bisa memikirkan GenServer sebagai abstraksi atau penolong yang mempermudah proses server. Pertama, Anda akan melihat bagaimana proses spawn menggunakan beberapa fungsi tingkat rendah. Kemudian kita akan beralih ke GenServer dan melihat bagaimana hal itu menyederhanakan sesuatu untuk kita dengan menghapus kebutuhan untuk menulis kode yang membosankan (dan cukup generik) setiap saat. Mari kita mulai!

Semuanya Dimulai dengan Spawn

Jika Anda bertanya kepada saya bagaimana membuat sebuah proses di Elixir, saya akan menjawab: spawn! spawn/1 adalah fungsi yang didefinisikan di dalam modul Kernel yang mengembalikan proses baru. Fungsi ini menerima lambda yang akan dieksekusi dalam proses pembuatannya. Begitu eksekusi selesai, prosesnya juga berjalan dengan baik:

Jadi, disini spawn kembali menjadi id proses baru. Jika Anda menambahkan delay ke lambda, string "hi" akan dicetak setelah beberapa saat:

Sekarang kita bisa spawn sebanyak mungkin proses yang kita inginkan, dan akan berjalan bersamaan:

Di sini kita spawing sepuluh proses dan mencetak test string dengan nomor acak. : rand adalah modul yang disediakan oleh Erlang, jadi namanya adalah sebuah atom. Yang keren adalah semua pesan akan dicetak pada saat bersamaan, setelah lima detik. Hal itu terjadi karena kesepuluh proses telah dilaksanakan secara bersamaan.

Bandingkan dengan contoh berikut yang melakukan tugas yang sama tapi tanpa menggunakan spawn/1:

Sementara kode ini berjalan, Anda bisa pergi ke dapur dan membuat secangkir kopi lagi karena akan memakan waktu hampir satu menit untuk menyelesaikannya. Setiap pesan ditampilkan secara berurutan, yang tentunya tidak optimal!

Anda mungkin bertanya: "Berapa banyak memori yang dikonsumsi sebuah proses?" Yah, itu tergantung, tapi awalnya itu menempati beberapa kilobyte, yang merupakan jumlah yang sangat kecil (bahkan laptop lamaku memiliki memori 8GB, belum lagi server modern yang keren).

Sejauh ini bagus. Sebelum kita mulai bekerja dengan GenServer, mari kita bahas satu hal penting lagi: menyampaikan dan menerima pesan.

Bekerja Dengan Pesan

Tidak mengherankan jika proses (yang terisolasi, seperti yang Anda ingat) perlu dikomunikasikan dengan cara tertentu, terutama bila menyangkut pengembangan sistem yang kurang lebih kompleks. Untuk mencapai hal ini, kita bisa menggunakan pesan.

Sebuah pesan dapat dikirim menggunakan fungsi dengan nama yang cukup jelas: send/2. Ini menerima tujuan (port, process id atau nama proses) dan pesan sebenarnya. Setelah pesan dikirim, pesan itu muncul di mailbox proses dan bisa diproses. Seperti yang Anda lihat, ide umumnya sangat mirip dengan aktivitas saling bertukar email sehari-hari kita.

Mailbox pada dasarnya adalah antrian "first in first out" (FIFO). Setelah pesan diproses, maka akan dihapus dari antrian. Untuk mulai menerima pesan, Anda perlu.. coba tebak! - a receive macro. Makro ini berisi satu atau lebih klausa, dan sebuah pesan dicocokkan dengan itu. Jika ditemukan kecocokan, pesan akan diproses. Jika tidak, pesan tersebut dimasukkan kembali ke dalam kotak pesan/mailbox. Selain itu, Anda dapat mengatur klausul after opsional yang berjalan jika pesan tidak diterima dalam waktu tertentu. Anda dapat membaca lebih lanjut tentang send/2 dan receive di dokumen resmi.

Oke, cukup dengan teori-mari kita coba bekerja dengan messages. Pertama-tama, kirimkan sesuatu ke proses saat ini:

Macro self/0 mengembalikan sebuah pid dari proses pemanggilan, yang persis seperti yang kita butuhkan. Jangan menghilangkan kurung bulat setelah fungsinya karena Anda akan mendapat peringatan terkait kecocokan ambiguitas.

Sekarang terima pesan sambil mengatur klausa after:

Perhatikan bahwa klausa mengembalikan hasil evaluasi baris terakhir, jadi kita mendapatkan string "hello!".

Ingat bahwa Anda dapat memperkenalkan sebanyak mungkin klausul yang diperlukan:

Di sini kita memiliki empat klausa: satu untuk menangani pesan sukses, yang lain menangani kesalahan, dan kemudian sebuah "fallback" clause dan timeout.

Jika pesan tidak sesuai dengan klausa, itu disimpan di kotak surat, yang tidak selalu diinginkan. Mengapa? Karena setiap kali ada pesan baru, yang lama diproses di first head (karena kotak surat itu adalah antrian FIFO), memperlambat program turun. Oleh karena itu sebuah "fallback" clause mungkin berguna.

Sekarang Anda tahu cara spawn proses, mengirim dan menerima pesan, mari kita lihat contoh yang sedikit lebih rumit yang melibatkan pembuatan server sederhana yang merespons berbagai pesan.

Bekerja dengan Proses Server

Pada contoh sebelumnya, kita hanya mengirim satu pesan, menerimanya, dan melakukan beberapa pekerjaan. Itu bagus, tapi tidak terlalu fungsional. Biasanya yang terjadi adalah kita punya server yang bisa merespon berbagai pesan. Dengan "server" maksud saya proses yang berjalan lama dibangun dengan fungsi berulang. Sebagai contoh, mari membuat server untuk melakukan beberapa persamaan matematika. Ini akan menerima pesan berisi operasi yang di reques dan beberapa argumen.

Mulailah dengan membuat server dan fungsi looping:

Jadi kita spawn proses yang terus mendengarkan pesan masuk. Setelah pesan diterima, fungsi listen/0 dipanggil kembali, sehingga membuat loop tak berujung. Di dalam fungsi listen/0, kita menambahkan dukungan untuk :sqrt message, yang akan menghitung akar kuadrat dari sebuah bilangan. arg akan berisi jumlah aktual untuk melakukan operasi melawan. Juga, kita mendefinisikan sebuah klausa fallback.

Anda sekarang dapat memulai server dan menetapkan id prosesnya ke variabel:

Brilliant! Sekarang mari kita tambahkan fungsi implementasi untuk benar-benar melakukan perhitungannya:

Gunakan fungsi ini sekarang:

Untuk saat ini, itu hanya mencetak argumen yang berlalu, jadi tweak kode Anda seperti ini untuk melakukan operasi matematika:

Sekarang, pesan lain dikirim ke server yang berisi hasil penghitungan.

Yang menarik adalah fungsi sqrt/2 hanya mengirim pesan ke server yang meminta untuk melakukan operasi tanpa menunggu hasilnya. Jadi, pada dasarnya, ia melakukan  asynchronous call.

Jelas, kita ingin meraih hasilnya pada beberapa titik waktu, jadi beri kode fungsi publik lainnya:

Sekarang memanfaatkannya:

Works! Tentu saja, Anda bahkan bisa membuat kumpulan server dan mendistribusikan tugas di antara itu, mencapai concurrency. Akan lebih mudah bila permintaan tidak berhubungan satu sama lain.

Temui GenServer

Baiklah, kita telah meliput beberapa fungsi yang memungkinkan kita membuat proses server yang sudah berjalan lama dan mengirim dan menerima pesan. Ini hebat, tapi kita harus menulis terlalu banyak kode boilerplate yang memulai loop server (start/0), menanggapi pesan (listen/0 private function), dan mengembalikan hasilnya (grab_result/0). Dalam situasi yang lebih kompleks, kita mungkin juga perlu mengutamakan shared state atau menangani kesalahan.

Seperti yang saya katakan di awal artikel, tidak perlu menemukan kembali sepeda. Sebagai gantinya, kita bisa memanfaatkan perilaku GenServer yang sudah menyediakan semua kode boilerplate untuk kita dan sangat mendukung proses server (seperti yang kita lihat di bagian sebelumnya).

Perilaku dalam Elixir adalah kode yang menerapkan pola yang umum. Untuk menggunakan GenServer, Anda perlu mendefinisikan modul callback khusus yang memenuhi kontrak seperti yang didikte oleh perilaku tersebut. Secara khusus, ia harus menerapkan beberapa fungsi callback, dan penerapan sebenarnya terserah Anda. Setelah callback ditulis, modul behavior dapat memanfaatkannya.

Seperti yang dinyatakan oleh dokumen, GenServer memerlukan enam callback untuk diimplementasikan, meskipun mereka memiliki implementasi default juga. Ini berarti bahwa Anda dapat mendefinisikan ulang hanya yang memerlukan beberapa custom logic.

Pertama hal yang pertama: kita perlu memulai server sebelum melakukan hal lain, jadi lanjutkan ke bagian selanjutnya!

Memulai Server

Untuk menunjukkan penggunaan GenServer, mari kita tulis CalcServer yang memungkinkan pengguna menerapkan berbagai operasi ke sebuah argumen. Hasil operasi akan disimpan dalam status server, dan kemudian operasi lain dapat diterapkan juga. Atau pengguna mungkin mendapatkan hasil akhir dari penghitungan.

Pertama-tama, gunakan makro penggunaan untuk plug in GenServer:

Sekarang kita perlu mendefinisikan kembali beberapa callback.

Yang pertama adalah init/1, yang dipanggil saat server dijalankan. Argumen yang dilewatkan digunakan untuk mengatur status server awal. Dalam kasus yang paling sederhana, callback ini harus mengembalikan tuple {:ok, initial_state}, meskipun ada nilai balik yang mungkin lainnya seperti {:stop, reason}, yang menyebabkan server berhenti.

Saya pikir kita dapat memungkinkan pengguna untuk menentukan keadaan awal untuk server kiya. Namun, kita harus memeriksa apakah argumen yang dilewatkan adalah angka. Jadi gunakan guard clause untuk itu:

Sekarang, cukup jalankan server dengan menggunakan fungsi start/3, dan berikan CalcServer Anda sebagai modul callback (argumen pertama). Argumen kedua akan menjadi keadaan awal:

Jika Anda mencoba melewati non-number sebagai argumen kedua, server tidak akan dimulai, itulah yang kita butuhkan.

Great! Setelah server kita berjalan, kita bisa memulai coding operasi matematika.

Menangani Permintaan Asynchronous

Permintaan asinkron disebut casts dalam istilah GenServer. Untuk melakukan permintaan seperti itu, gunakan fungsi cast/2, yang menerima server dan permintaan sebenarnya. Hal ini mirip dengan sqrt/2 function yang kita kode ketika berbicara tentang proses server. Ini juga menggunakan pendekatan "fire and forget", yang berarti kita tidak menunggu permintaan selesai.

Untuk menangani pesan asinkron, handle_cast/2 callback digunakan. Ini menerima permintaan dan status dan harus menanggapi dengan tuple {:noreply, new_state} dalam kasus yang paling sederhana (atau {:stop, reason, new_state} untuk menghentikan loop server). Sebagai contoh, mari kita menangani sebuah asynchronous :sqrt cast:

Begitulah cara kita menjaga keadaan server kita. Awalnya nomor (dilewatkan saat server sudah dimulai) adalah 5.1. Sekarang kita mengupdate state dan set ke :math.sqrt(5.1).

Kode fungsi interface yang memanfaatkan cast/2:

Bagi saya, ini menyerupai penyihir jahat yang mengeluarkan mantra namun tidak peduli dengan dampak yang ditimbulkannya.

Perhatikan bahwa kita memerlukan id proses untuk melakukan cast. Ingat bahwa ketika server berhasil dijalankan, sebuah tuple {:ok, pid} dikembalikan. Karena itu, mari kita gunakan pencocokan pola untuk mengekstrak id proses:

Nice! Pendekatan yang sama dapat digunakan untuk menerapkan, katakanlah, perkalian. Kode akan sedikit lebih kompleks karena kita harus melewati argumen kedua, pengganda:

Fungsi cast hanya mendukung dua argumen, jadi saya perlu membuat tuple dan meneruskan argumen tambahan di sana.

Sekarang callback:

Kita juga bisa menulis callback handle_cast tunggal yang mendukung operasi sekaligus menghentikan server jika operasi tidak diketahui:

Sekarang gunakan fungsi interface baru:

Hebat, tapi saat ini tidak ada cara untuk mendapatkan hasil penghitungan. Oleh karena itu, sekarang saatnya untuk menentukan callback lagi.

Menangani Permintaan Synchronous

Jika permintaan asinkron dimuat, maka yang sinkron diberi nama calls. Untuk menjalankan permintaan seperti itu, gunakan fungsi call/3, yang menerima server, permintaan, dan batas waktu opsional yang sama dengan lima detik secara default.

Permintaan sinkron digunakan saat kita ingin menunggu sampai respon benar-benar tiba dari server. Kasus penggunaan yang khas adalah mendapatkan beberapa informasi seperti hasil penghitungan, seperti pada contoh hari ini (ingat fungsi grab_result/0 dari salah satu bagian sebelumnya).

Untuk memproses permintaan sinkron, callback handle_call/3 digunakan. Ini menerima permintaan, sebuah tuple yang berisi pid server, dan sebuah istilah yang mengidentifikasi panggilan, dan juga status saat ini. Dalam kasus yang paling sederhana, seharusnya merespons dengan tuple {:reply, reply, new_state}.

Kode Code this callback now:

Seperti yang Anda lihat, tidak ada yang rumit. replay dan status baru sama dengan status saat ini karena saya tidak ingin mengubah apapun setelah hasilnya dikembalikan.

Sekarang interface result/1 function:

Ini dia! Penggunaan akhir CalcServer ditunjukkan di bawah ini:

Aliasing

Menjadi agak membosankan untuk selalu menyediakan id proses saat memanggil fungsi interface. Untungnya, dimungkinkan untuk memberi nama proses Anda, atau alias. Hal ini dilakukan pada awal server dengan menetapkan name:

Perhatikan bahwa saya tidak menyimpan pid sekarang, meskipun Anda mungkin ingin melakukan pencocokan pola untuk memastikan server tersebut benar-benar telah dimulai.

Sekarang fungsi interface menjadi sedikit lebih sederhana:

Jangan lupa bahwa Anda tidak bisa memulai dua server dengan alias yang sama.

Sebagai alternatif, Anda dapat mengenalkan fungsi interface lain start/1 di dalam modul Anda dan memanfaatkan makro __MODULE__/0, yang mengembalikan nama modul saat ini sebagai sebuah atom:

Penghentian

Callback lain yang dapat didefinisikan ulang dalam modul Anda disebut terminate/2. Ini menerima alasan dan status saat ini, dan ini dipanggil saat server akan keluar. Ini mungkin terjadi ketika, misalnya, Anda melewatkan argumen yang salah ke fungsi interface multiply/1:

Callback mungkin terlihat seperti ini:

Kesimpulan

Pada artikel ini kita telah membahas dasar-dasar concurrency di Elixir dan membahas fungsi dan makro seperti spawn, receive, dan send. Anda telah mempelajari proses apa, bagaimana membuatnya, dan cara mengirim dan menerima pesan. Selain itu, kami telah melihat bagaimana membangun proses server berjalan lama yang sederhana yang merespons pesan sinkron dan asinkron.

Selain itu, kami telah membahas perilaku GenServer dan telah melihat bagaimana menyederhanakan kode dengan mengenalkan berbagai callback. Kami telah bekerja dengan callback init, terminate, handle_call dan handle_cast dan membuat server penghitung sederhana. Jika ada sesuatu yang tidak jelas bagi Anda, jangan ragu untuk mengirimkan pertanyaan Anda!

Masih ada lagi GenServer, dan tentu saja tidak mungkin meliput semuanya dalam satu artikel. Di postingan berikutnya, saya akan menjelaskan apa itu supervisor dan bagaimana Anda bisa menggunakannya untuk memantau proses Anda dan memulihkannya dari kesalahan. Sampai kemudian, happy coding!

Advertisement
Did you find this post useful?
Want a weekly email summary?
Subscribe below and we’ll send you a weekly email summary of all new Code tutorials. Never miss out on learning about the next big thing.
Advertisement
Looking for something to help kick start your next project?
Envato Market has a range of items for sale to help get you started.