BEAM menjalankan setiap program Elixir di atas process, yaitu unit eksekusi ringan yang memiliki memori sendiri dan berkomunikasi dengan process lain hanya melalui pesan. Topik ini membahas tiga primitif yang menjadi dasar seluruh konkurensi Elixir, yaitu spawn/1 untuk membuat process baru, send/2 untuk mengirim pesan, dan receive/1 untuk membaca pesan. Selain ketiganya, self/0 diperkenalkan sebagai cara mengambil identitas process yang sedang berjalan. Contoh dalam topik ini berupa skrip .exs yang dapat dijalankan dengan perintah elixir.
Process sebagai unit eksekusi BEAM#
Process pada BEAM bukan proses sistem operasi. Sebuah process dibuat dan dihancurkan dengan biaya yang sangat kecil, sehingga satu node BEAM dapat menampung ratusan ribu process sekaligus, bahkan jutaan, bergantung pada memori yang tersedia. Setiap process memiliki tumpukan memori sendiri dan melakukan pengumpulan sampah memori (garbage collection) secara terpisah, sehingga pekerjaan satu process tidak membebani process lain secara langsung. Karena tidak ada memori yang dibagi, satu-satunya cara bagi dua process untuk berbagi data adalah saling mengirim pesan.
Pembuatan process dilakukan melalui spawn/1, yaitu fungsi yang menerima satu fungsi tanpa nama (anonymous function) dan menjalankannya dalam process baru. Fungsi spawn/1 mengembalikan pid (process identifier), yaitu identitas process yang berbentuk nilai khusus, misalnya #PID<0.108.0>. Angka di dalam pid ditetapkan saat process dibuat, sehingga pid untuk program yang sama berbeda pada setiap eksekusi. Sementara itu, self/0 mengembalikan pid process pemanggilnya.
Karena pid tidak sama antara dua eksekusi, contoh dalam topik ini tidak pernah mencetak pid secara langsung. Sebagai gantinya, properti pid diuji dengan is_pid/1 yang menghasilkan nilai tetap:
pid = spawn(fn -> :ok end)
IO.puts("spawn mengembalikan pid: #{is_pid(pid)}")spawn mengembalikan pid: trueMengirim pesan ke mailbox#
send/2 mengirimkan sebuah pesan ke process tujuan. Pesan dapat berupa nilai Elixir apa pun, dan bentuk yang lazim adalah tuple yang elemen pertamanya berupa atom, misalnya {:cetak, "teks"}, sehingga penerima mudah mengenali jenis pesannya. Pemanggilan send/2 bersifat asinkron: pengirim tidak menunggu pesan diproses, karena send/2 hanya menambahkan pesan ke mailbox penerima lalu langsung kembali. Fungsi ini bahkan mengembalikan pesannya sendiri, sehingga pemanggilan dapat disambungkan dalam pipeline.
Perilaku asinkron ini membawa konsekuensi penting. Mengirim pesan ke process yang sudah berhenti tidak memunculkan error, dan pesannya dibuang tanpa pemberitahuan. Selain pid, send/2 juga menerima nama terdaftar, yaitu atom yang dipasangkan dengan sebuah process melalui Process.register/2. Bagian ini hanya menggunakan pid agar mekanismenya terlihat jelas.
Pengiriman paling mudah diamati ketika tujuannya adalah process pengirim itu sendiri. Fungsi self/0 menghasilkan pid process yang sedang berjalan, sehingga send(self(), pesan) meletakkan pesan di mailbox miliknya sendiri, lalu pesan itu dibaca kembali oleh receive:
send(self(), {:sapaan, "hai"})
receive do
{:sapaan, teks} -> IO.puts("pesan dibaca dari mailbox: #{teks}")
after
1000 -> IO.puts("waktu habis")
endpesan dibaca dari mailbox: haiMembaca pesan dengan receive#
receive/1 membaca pesan dari mailbox process yang sedang berjalan. Sintaksnya menyerupai case, yaitu deretan klausa pola -> tubuh yang diuji berurutan. Ketika receive dieksekusi, mailbox dipindai dari pesan tertua ke termuda, dan pesan pertama yang cocok dengan salah satu pola diambil keluar dari antrean untuk diproses. Apabila tidak ada pesan yang cocok, receive menunggu pesan baru datang, dan pesan-pesan lama yang tidak cocok tetap tersimpan di mailbox.
Menunggu tanpa batas berbahaya apabila pesan yang diharapkan ternyata tidak pernah datang. Karena itu, receive menyediakan klausa after yang menentukan lama menunggu dalam milidetik. Apabila batas waktu tercapai dan tidak ada pesan yang cocok, tubuh after dijalankan. Pola after 1000 -> :waktu_habis pada contoh-contoh topik ini digunakan sebagai pengaman yang seragam:
receive do
:tidak_akan_datang -> IO.puts("pesan diterima")
after
300 -> IO.puts("waktu tunggu 300 milidetik habis")
endwaktu tunggu 300 milidetik habisSkrip utuh: pertukaran pesan dua arah#
Contoh lengkap berikut menghubungkan dua process untuk saling berkirim pesan. Process utama membuat sebuah process pekerja, mengirimkan perintah {:cetak, teks}, lalu menunggu balasannya. Pekerja mengubah teks menjadi huruf besar dengan String.upcase/1 dan mengirimkan hasilnya kembali sebagai {:jawaban, teks}:
# pos.exs — process pekerja menerima {:cetak, teks} lalu membalas {:jawaban, huruf besar}
induk = self()
pekerja =
spawn(fn ->
receive do
{:cetak, teks} ->
send(induk, {:jawaban, String.upcase(teks)})
after
5000 -> :waktu_habis
end
end)
send(pekerja, {:cetak, "selamat belajar"})
receive do
{:jawaban, hasil} ->
IO.puts("balasan dari process pekerja: #{hasil}")
after
1000 ->
IO.puts("waktu habis")
endbalasan dari process pekerja: SELAMAT BELAJARBagian yang paling menentukan terletak pada variabel induk. Di dalam fungsi yang dikirim kepada spawn/1, pemanggilan self/0 menghasilkan pid pekerja, bukan pid pengirim, karena self/0 selalu merujuk pada process yang menjalankan pemanggilannya. Karena itu, pid process utama diambil lebih dahulu melalui induk = self(), lalu nilai itu tertangkap oleh fungsi tanpa nama yang diteruskan ke spawn/1. Pekerja kemudian membalas ke induk, dan process utama membaca balasan itu dengan pola {:jawaban, hasil}. Klausa after pada kedua receive membuat skrip berhenti dengan sendirinya walaupun ada pesan yang tidak pernah sampai.
Kesalahan umum#
Dua kesalahan paling sering terjadi pada tahap ini. Pertama, membalas pesan dengan self/0 dari dalam process pekerja. Balasan yang dikirim dengan cara itu mendarat di mailbox pekerja sendiri, bukan di mailbox pengirim, sehingga pihak yang menunggu tidak pernah menerima apa pun. Perbaikannya adalah menangkap pid pengirim sebelum pemanggilan spawn/1, seperti pada pos.exs, atau menyertakan pid pengirim dalam isi pesannya.
Kedua, bentuk pesan yang dikirim tidak sama dengan pola yang ditunggu. Apabila pesan yang dikirim berupa :tambah sedangkan penerima mencocokkan {:tambah, nilai}, receive terus menunggu sementara pesan asing menumpuk di mailbox. Program tidak berhenti, tetapi tidak ada pekerjaan yang dilakukan. Perbaikannya adalah menyamakan bentuk pesan di kedua sisi, lalu memeriksa isi mailbox dengan pembacaan defensif yang dibahas pada topik timeout dan mailbox.
Ringkasan#
spawn/1menjalankan sebuah fungsi dalam process baru dan mengembalikan pid-nya, sedangkanself/0mengembalikan pid process yang sedang berjalan.send/2menambahkan pesan ke mailbox process tujuan secara asinkron, tidak memunculkan error apabila tujuan sudah berhenti, dan menerima pid maupun nama terdaftar.receive/1mengambil dari mailbox pesan pertama yang cocok dengan salah satu polanya, dan klausaaftermembatasi lama menunggu agar program tidak menggantung.
Referensi#
- Kernel — Hexdocs Elixir
— dokumentasi fungsi bawaan
spawn/1,send/2,self/0, danreceive/1. - Modul Process — Hexdocs Elixir — rincian fungsi tingkat process, termasuk pengujian pid.
- Processes — Hexdocs Elixir — panduan resmi tentang process, spawn, dan pertukaran pesan pada BEAM.