Mengubah Server Single-Threaded Kita Menjadi Server Multithreaded
Saat ini, server kita memproses setiap request secara bergantian (in turn), yang berarti dia tidak bakal memproses koneksi yang kedua sebelum proses untuk koneksi yang pertama selesai (finished processing). Kalau server ini nerima makin banyak requests, eksekusi secara berurutan (serial execution) kayak gini bakal jadi makin kurang optimal. Kalau server ini nerima sebuah request yang makan waktu lama sekali buat diproses, requests yang masuk berikutnya (subsequent requests) bakal terpaksa harus nungguin sampai request lama itu selesai, biarpun requests yang baru ini sebenernya bisa aja diproses dengan cepat. Kita perlu benerin ini nih, tapi pertama-tama mari kita ngelihat aksinya masalah ini secara langsung.
Menyimulasikan Sebuah Request yang Pelan (Slow Request)
Kita bakal ngelihat gimana sebuah request yang pemrosesannya lambat bisa berdampak sama requests lainnya yang dilakuin ke implementasi server kita saat ini. Listing 21-10 mengimplementasikan penanganan sebuah request ke path /sleep dengan menyimulasikan (simulated) sebuah balasan yang pelan yang mana bakal ngebikin server kita ini sleep (tidur/berhenti sejenak) selama lima detik sebelum dia ngasih balasan (responding).
use std::{
fs,
io::{BufReader, prelude::*},
net::{TcpListener, TcpStream},
thread,
time::Duration,
};
// --snip--
fn main() {
let listener = TcpListener::bind("127.0.0.1:7878").unwrap();
for stream in listener.incoming() {
let stream = stream.unwrap();
handle_connection(stream);
}
}
fn handle_connection(mut stream: TcpStream) {
// --snip--
let buf_reader = BufReader::new(&stream);
let request_line = buf_reader.lines().next().unwrap().unwrap();
let (status_line, filename) = match &request_line[..] {
"GET / HTTP/1.1" => ("HTTP/1.1 200 OK", "hello.html"),
"GET /sleep HTTP/1.1" => {
thread::sleep(Duration::from_secs(5));
("HTTP/1.1 200 OK", "hello.html")
}
_ => ("HTTP/1.1 404 NOT FOUND", "404.html"),
};
// --snip--
let contents = fs::read_to_string(filename).unwrap();
let length = contents.len();
let response =
format!("{status_line}\r\nContent-Length: {length}\r\n\r\n{contents}");
stream.write_all(response.as_bytes()).unwrap();
}
Kita beralih (switched) dari yang asalnya pakai if jadi match karena sekarang
kita punya tiga kemungkinan kasus (cases). Kita perlu secara eksplisit memakai
pencocokan (match) pada sebuah slice dari request_line buat mencocokkan pattern
(pattern-match) dengan nilai-nilai string literal tersebut; match itu tidak
secara otomatis ngelakuin referencing dan dereferencing kayak yang dilakuin sama
method equality (pemeriksaan kesamaan ==).
Arm (lengan) pertama ini bunyinya sama kayak isi dari blok if yang ada di Listing
21-9. Arm kedua cocok dengan sebuah request ke path /sleep. Pas request ini
diterima, server kita ini bakal tidur (sleep) selama lima detik sebelum dia nge-render
halaman HTML yang isinya sukses tersebut. Arm yang ketiga bunyinya sama persis kayak
blok else dari Listing 21-9.
Kita bisa ngelihat sendiri betapa primitifnya (primitive) server kita ini: libraries sungguhan (real libraries) itu biasanya nanganin proses rekognisi (pengenalan) banyak requests dengan cara yang jauh tidak lebih verbose (kepanjangan nulisnya) dari ini!
Jalanin servernya memakai cargo run. Terus buka dua jendela (windows) browser:
satu buat http://127.0.0.1:7878 dan satu lagi buat http://127.0.0.1:7878/sleep.
Kalau kita masukin URI / beberapa kali kayak sebelumnya, kita bakal ngelihat kalau dia
nge-responsnya cepet sekali. Tapi kalau kita masukin /sleep dan kemudian muat (load)
/ di tab lain, kita bakal ngelihat kalau / ini terpaksa harus nungguin (waits)
sampai si sleep tadi selesai tidur selama durasi penuh lima detiknya sebelum
halamannya bisa dimuat.
Ada banyak teknik yang bisa kita pakai buat ngehindarin situasi di mana requests
ini pada numpuk (backing up) di belakang sebuah request yang pelan, termasuk
salah satunya dengan memakai async kayak yang udah kita lakuin di Bab 17;
sementara yang bakal kita implementasikan di sini adalah sebuah thread pool
(kumpulan utas).
Meningkatkan Throughput dengan Sebuah Thread Pool
Sebuah thread pool itu adalah sekelompok threads yang udah ditelurkan (spawned) yang lagi bersiap-siap dan standby nungguin buat nanganin sebuah tugas (task). Saat program tersebut nerima tugas yang baru, dia bakal ngasih salah satu threads yang ada di dalam kolam (pool) ini buat ngerjain tugas tersebut, dan thread itulah yang bakal memprosesnya. Sisa threads lainnya yang ada di dalam pool ini tetep tersedia (available) buat nanganin tugas-tugas lain yang masuk saat thread yang pertama tadi lagi sibuk memproses. Pas thread pertama udah beres ngerjain tugasnya, dia dikembaliin (returned) lagi ke dalam pool yang isinya threads nganggur (idle threads), terus dia siap (ready) buat nanganin tugas baru lagi. Sebuah thread pool memungkinkan kita buat memproses banyak koneksi secara konkuren (bersamaan), ningkatin throughput (kemampuan nangani permintaan) dari server kita.
Kita bakal membatasi (limit) jumlah threads yang ada di dalam pool ini menjadi angka yang kecil buat melindungi (protect) kita dari serangan DoS (Denial of Service); kalau kita ngebikin program kita buat netasin (create) thread baru buat setiap kali ada request yang masuk, seseorang yang ngebikin 10 juta requests ke server kita bisa-bisa bikin kekacauan parah dengan cara ngabisin semua sumber daya (resources) server kita lalu bikin semua pemrosesan requests jadi mandek total (grinding to a halt).
Jadi ketimbang menelurkan threads tanpa batas (unlimited threads), kita bakal
punya jumlah threads yang tetap (fixed number) yang pada standby nungguin di
dalam pool tersebut. Requests yang masuk bakal dikirimin ke dalam pool ini
buat diproses. Pool ini bakal memelihara sebuah antrean (queue) yang isinya requests
yang baru masuk. Masing-masing dari threads yang ada di dalam pool ini bakal
mengambil (pop off) satu request dari antrean ini, menangani request tersebut,
dan lalu minta satu request lagi ke antrean tersebut. Pakai desain kayak gini,
kita bisa memproses maksimal N requests secara konkuren, di mana N itu
adalah jumlah threads yang ada. Kalau setiap thread itu lagi sibuk merespons ke
requests yang jalan lama sekali, requests yang masuk berikutnya emang masih
tetap bisa pada numpuk di dalem antreannya, tapi kita udah ningkatin seberapa banyak jumlah
requests yang jalannya lama sekali yang sanggup kita tangani sebelum kita nyampe ke titik
jenuh tersebut.
Teknik ini itu hanyalah salah satu dari sekian banyak cara yang ada buat ningkatin throughput dari sebuah web server. Opsi-opsi lain yang mungkin bisa kita eksplorasi adalah model fork/join, model single-threaded async I/O, sama model multithreaded async I/O. Kalau kita tertarik sama topik ini, kita bisa ngebaca lebih lanjut soal solusi-solusi lainnya dan nyobain mengimplementasikan mereka; dengan bahasa pemrograman tingkat rendah (low-level) kayak Rust ini, semua opsi ini sangat mungkin sekali buat dikerjain (possible).
Sebelum kita mulai mengimplementasikan sebuah thread pool, mari kita obrolin kayak gimana rupa dari memakai si pool ini nantinya (what using the pool should look like). Pas kita lagi mencoba mendesain (design) sebuah kode, menulis interface client-nya terlebih dahulu bisa ngebantu memandu jalannya desain kita. Tulis API dari kodenya sehingga strukturnya itu udah sesuai dengan cara kita manggil dia nantinya; baru deh setelah itu implementasikan fungsionalitasnya di dalam struktur tersebut ketimbang mikirin fungsionalitasnya duluan baru mikirin desain API public-nya belakangan.
Mirip dengan gimana kita memakai test-driven development (pengembangan berbasis pengujian) di dalam project kita pas Bab 12 kemarin, kita bakal memakai compiler-driven development (pengembangan berbasis compiler) di sini. Kita bakal nulis kode yang manggil fungsi-fungsi yang pengen kita panggil, lalu baru deh kita ngelihat ke error-error yang dikasih sama compiler buat nentuin apa yang harus kita ubah berikutnya supaya kodenya bisa benar-benar jalan. Tapi sebelum kita melakukan itu, kita bakal nyelidikin teknik yang tidak bakal kita pakai dulu sebagai titik mulai kita.
Menelurkan (Spawning) Sebuah Thread Buat Setiap Request
Pertama-tama, mari kita eksplorasi kira-kira kayak gimana kelihatannya kode kita ini kalau seandainya dia benar-benar ngebikin thread baru buat setiap koneksi yang masuk. Seperti yang udah kita sebutin sebelumnya, ini itu bukan rencana akhir kita gara-gara ada masalah yang mana kita berpotensi bakal netasin threads dalam jumlah yang tidak terbatas, tapi cara ini adalah titik pijak (starting point) yang oke buat ngebikin supaya server multithreaded kita ini bisa jalan dulu. Nanti barulah kita tambahin thread pool sebagai sebuah perbaikan (improvement), dan jadinya membandingkan (contrasting) kedua buah solusi ini bakal jadi lebih gampang.
Listing 21-11 nunjukin beberapa perubahan yang perlu dibikin di dalam fungsi main
supaya dia menelurkan (spawn) sebuah thread baru buat nanganin masing-masing
stream yang ada di dalam loop for tersebut.
use std::{
fs,
io::{BufReader, prelude::*},
net::{TcpListener, TcpStream},
thread,
time::Duration,
};
fn main() {
let listener = TcpListener::bind("127.0.0.1:7878").unwrap();
for stream in listener.incoming() {
let stream = stream.unwrap();
thread::spawn(|| {
handle_connection(stream);
});
}
}
fn handle_connection(mut stream: TcpStream) {
let buf_reader = BufReader::new(&stream);
let request_line = buf_reader.lines().next().unwrap().unwrap();
let (status_line, filename) = match &request_line[..] {
"GET / HTTP/1.1" => ("HTTP/1.1 200 OK", "hello.html"),
"GET /sleep HTTP/1.1" => {
thread::sleep(Duration::from_secs(5));
("HTTP/1.1 200 OK", "hello.html")
}
_ => ("HTTP/1.1 404 NOT FOUND", "404.html"),
};
let contents = fs::read_to_string(filename).unwrap();
let length = contents.len();
let response =
format!("{status_line}\r\nContent-Length: {length}\r\n\r\n{contents}");
stream.write_all(response.as_bytes()).unwrap();
}
Kayak yang udah kita pelajarin di Bab 16, thread::spawn itu bakal ngebikin thread
baru lalu dia bakal ngejalanin kode yang ada di dalam closure tersebut di dalem si thread
yang baru ini. Kalau kita jalanin kode ini dan memuat /sleep di browser kita, lalu buka
/ di dua tab browser yang lain, kita benar-benar bakal ngelihat kalau requests ke / itu
tidak perlu lagi nungguin si /sleep sampai selesai beres (finish). Namun, seperti yang
tadi udah kita sebutin, cara ini pada akhirnya bakal bikin sistemnya kewalahan (overwhelm
the system) karena kita bakal terus-terusan ngebikin threads baru tanpa ada batas
sama sekali.
Kita juga mungkin masih inget dari Bab 17 kalau ini itu adalah tipe-tipe situasi persis yang mana async dan await bakal benar-benar bersinar! Simpan pikiran itu di kepala kita ya selagi kita ngebangun thread pool ini dan coba renungkan (think about) gimana situasinya bakal kelihatan berbeda atau malah sama aja kalau kita pakai async.
Ngebikin Sejumlah Threads dalam Jumlah Terbatas (Finite Number of Threads)
Kita mau supaya thread pool kita ini bekerja dengan cara yang mirip-mirip dan kerasa
familier supaya pindah (switching) dari threads biasa ke thread pool ini tidak butuh
perubahan gede-gedean pada kode-kode yang memakai API kita. Listing 21-12 nunjukin
antarmuka bayangan (hypothetical interface) buat sebuah struct ThreadPool yang pengen
kita pakai ketimbang thread::spawn.
use std::{
fs,
io::{BufReader, prelude::*},
net::{TcpListener, TcpStream},
thread,
time::Duration,
};
fn main() {
let listener = TcpListener::bind("127.0.0.1:7878").unwrap();
let pool = ThreadPool::new(4);
for stream in listener.incoming() {
let stream = stream.unwrap();
pool.execute(|| {
handle_connection(stream);
});
}
}
fn handle_connection(mut stream: TcpStream) {
let buf_reader = BufReader::new(&stream);
let request_line = buf_reader.lines().next().unwrap().unwrap();
let (status_line, filename) = match &request_line[..] {
"GET / HTTP/1.1" => ("HTTP/1.1 200 OK", "hello.html"),
"GET /sleep HTTP/1.1" => {
thread::sleep(Duration::from_secs(5));
("HTTP/1.1 200 OK", "hello.html")
}
_ => ("HTTP/1.1 404 NOT FOUND", "404.html"),
};
let contents = fs::read_to_string(filename).unwrap();
let length = contents.len();
let response =
format!("{status_line}\r\nContent-Length: {length}\r\n\r\n{contents}");
stream.write_all(response.as_bytes()).unwrap();
}
ThreadPool ideal milik kitaKita memakai ThreadPool::new buat ngebikin sebuah thread pool baru dengan jumlah
threads yang bisa dikonfigurasi, yang di kasus ini yaitu empat biji. Terus, di dalam
loop for, si pool.execute ini punya antarmuka (interface) yang mirip sekali
sama thread::spawn karena dia juga nerima sebuah closure yang mana seharusnya bakal
dijalanin sama si pool tersebut buat setiap stream yang masuk. Kita perlu mengimplementasikan
pool.execute ini sedemikian rupa sehingga dia bakal ngambil closure yang diterimanya
itu terus ngasihin itu ke salah satu thread yang ada di dalam pool buat dijalanin.
Kode ini jelas masih belum bisa di-compile, tapi kita bakal nyoba men-compile-nya
supaya si compiler bisa memandu (guide) kita soal gimana caranya ngeberesin ini.
Ngebangun ThreadPool Memakai Compiler-Driven Development (Pengembangan Berbasis Compiler)
Silakan bikin perubahan-perubahan yang ada di Listing 21-12 ke file src/main.rs kita,
dan lalu mari kita pakai pesan-pesan error compiler yang asalnya dari cargo check
buat mengarahkan jalan (drive) dari proses development kita. Ini dia error
pertama yang kita dapet:
$ cargo check
Checking hello v0.1.0 (file:///projects/hello)
error[E0433]: failed to resolve: use of undeclared type `ThreadPool`
--> src/main.rs:11:16
|
11 | let pool = ThreadPool::new(4);
| ^^^^^^^^^^ use of undeclared type `ThreadPool`
For more information about this error, try `rustc --explain E0433`.
error: could not compile `hello` (bin "hello") due to 1 previous error
Sip sekali! (Great!) Error ini ngasih tahu kita kalau kita ini butuh punya tipe atau
modul ThreadPool, jadi kita bakal ngebangunnya sekarang juga. Implementasi ThreadPool
kita ini sifatnya bakal independen (independent) dan tidak peduli apa jenis kerjaan
yang lagi dilakuin sama web server kita ini. Jadi mari kita alihkan (switch)
crate hello kita ini dari yang tadinya sebuah binary crate menjadi sebuah
library crate buat nampung kode implementasi ThreadPool kita ini. Setelah kita ngubah
dia jadi library crate, kita juga jadi bisa lho memakai library thread pool yang
udah terpisah ini buat sekiranya ada pekerjaan apa pun lainnya yang mau kita lakuin pakai
sebuah thread pool, bukannya cuma khusus buat ngelayanin (serving) web requests doang.
Bikin sebuah file src/lib.rs yang isinya mengandung yang berikut ini, yang mana ini
adalah definisi paling simpel yang bisa kita punya buat sebuah struct ThreadPool buat
saat ini:
pub struct ThreadPool;
Terus edit file main.rs kita buat ngebawa (bring) ThreadPool tersebut masuk ke
dalam scope yang asalnya dari library crate dengan nambahin kode berikut ke bagian
paling atas (top) dari src/main.rs:
use hello::ThreadPool;
use std::{
fs,
io::{BufReader, prelude::*},
net::{TcpListener, TcpStream},
thread,
time::Duration,
};
fn main() {
let listener = TcpListener::bind("127.0.0.1:7878").unwrap();
let pool = ThreadPool::new(4);
for stream in listener.incoming() {
let stream = stream.unwrap();
pool.execute(|| {
handle_connection(stream);
});
}
}
fn handle_connection(mut stream: TcpStream) {
let buf_reader = BufReader::new(&stream);
let request_line = buf_reader.lines().next().unwrap().unwrap();
let (status_line, filename) = match &request_line[..] {
"GET / HTTP/1.1" => ("HTTP/1.1 200 OK", "hello.html"),
"GET /sleep HTTP/1.1" => {
thread::sleep(Duration::from_secs(5));
("HTTP/1.1 200 OK", "hello.html")
}
_ => ("HTTP/1.1 404 NOT FOUND", "404.html"),
};
let contents = fs::read_to_string(filename).unwrap();
let length = contents.len();
let response =
format!("{status_line}\r\nContent-Length: {length}\r\n\r\n{contents}");
stream.write_all(response.as_bytes()).unwrap();
}
Kode ini tentu masih belum bisa jalan ya, tapi mari kita cek (check) kodenya lagi buat dapetin pesan error selanjutnya yang perlu kita beresin (address):
$ cargo check
Checking hello v0.1.0 (file:///projects/hello)
error[E0599]: no function or associated item named `new` found for struct `ThreadPool` in the current scope
--> src/main.rs:12:28
|
12 | let pool = ThreadPool::new(4);
| ^^^ function or associated item not found in `ThreadPool`
For more information about this error, try `rustc --explain E0599`.
error: could not compile `hello` (bin "hello") due to 1 previous error
Error ini mengindikasikan kalau langkah selanjutnya yang perlu kita lakuin adalah
ngebikin sebuah fungsi associated bernama new untuk si ThreadPool ini. Kita
juga tahu kalau new ini butuh satu parameter yang mana bisa menerima angka 4
sebagai argumen dan harus ngembaliin sebuah instance ThreadPool.
Mari kita implementasikan fungsi new yang paling simpel yang punya karakteristik
kayak gitu:
pub struct ThreadPool;
impl ThreadPool {
pub fn new(size: usize) -> ThreadPool {
ThreadPool
}
}
Kita milih usize sebagai tipe buat parameter size karena kita tahu kalau ngebikin
threads dengan jumlah minus (negative number) itu emang kedengerannya
tidak masuk akal (doesn’t make sense). Kita juga tahu kalau kita bakal makek angka
4 ini sebagai ukuran jumlah elemen di dalam sebuah koleksi (collection) yang
isinya threads, yang mana itu adalah tujuan asli kenapa tipe usize dibikin, kayak yang udah
kita obrolin di “Tipe-tipe Angka Bulat (Integer Types)” di Bab 3.
Mari kita cek kodenya lagi:
$ cargo check
Checking hello v0.1.0 (file:///projects/hello)
error[E0599]: no method named `execute` found for struct `ThreadPool` in the current scope
--> src/main.rs:17:14
|
17 | pool.execute(|| {
| -----^^^^^^^ method not found in `ThreadPool`
For more information about this error, try `rustc --explain E0599`.
error: could not compile `hello` (bin "hello") due to 1 previous error
Sekarang errornya kejadian gara-gara kita tidak punya method execute pada
struct ThreadPool kita. Ingat kembali materi dari “Ngebikin Sejumlah Threads dalam
Jumlah Terbatas (Finite Number of Threads)” tadi
di mana kita memutuskan kalau thread pool kita ini seharusnya punya interface (antarmuka)
yang mirip sama thread::spawn. Selain itu, kita bakal mengimplementasikan fungsi
execute ini sedemikian rupa sehingga dia nerima closure yang udah dikasih ke dia
lalu mengopernya ke sebuah thread yang lagi nganggur (idle thread) di dalam si pool
tersebut buat dijalanin.
Kita bakal mendefinisikan method execute pada ThreadPool ini supaya dia menerima
sebuah closure sebagai parameter. Ingat kembali dari “Mengoper Nilai yang Ditangkap
Keluar dari Closure dan Trait Fn” di Bab 13 kalau kita bisa nerima closures
sebagai parameter yang memakai tiga jenis trait yang berbeda: yaitu Fn, FnMut, dan
FnOnce. Kita harus memutuskan trait closure mana yang mau dipakai di sini. Kita tahu
kalau ujung-ujungnya kita ini bakal ngerjain hal yang mirip-mirip sama yang dilakuin
oleh implementasi thread::spawn punya standard library, jadi kita bisa nyontek (look at)
batasan-batasan (bounds) apa aja yang dipunyai sama signature si thread::spawn ini pada parameternya.
Dokumentasi di sana ngasih tahu kita hal berikut:
pub fn spawn<F, T>(f: F) -> JoinHandle<T>
where
F: FnOnce() -> T,
F: Send + 'static,
T: Send + 'static,
Parameter bertipe F inilah yang lagi jadi fokus (concerned with) kita di sini;
sedangkan parameter bertipe T itu ada kaitannya sama nilai kembalian
(return value) dari fungsi itu, yang mana itu tidak jadi masalah buat kita. Kita bisa
ngelihat kalau si spawn ini memakai FnOnce sebagai trait bound (batasan trait)
buat F-nya. Ini juga merupakan trait yang kemungkinan besar kita inginkan, karena
nantinya argumen closure yang kita dapat di dalam execute ini juga pada akhirnya
bakal kita operin (pass) ke dalam spawn. Kita bisa makin yakin kalau FnOnce itu
adalah trait yang benar-benar pengen kita pake soalnya thread yang dijalanin buat
nanganin satu request itu emang cuma bakal mengeksekusi closure buat request tersebut
sebanyak satu kali doang, yang mana ya cocok persis (matches) sama embel-embel kata
Once (sekali) di dalam trait FnOnce.
Parameter bertipe F itu juga punya trait bound Send dan lifetime bound
(batasan rentang hidup) 'static, yang mana emang sangat berguna buat situasi kita
saat ini: kita butuh trait Send ini buat mindahin (transfer) si closure
ini dari satu thread ke thread yang lainnya dan 'static ini gara-gara kita tidak
tahu seberapa lama waktu yang dibutuhkan sama si thread tersebut buat selesai
melakukan eksekusi kodenya. Mari kita bikin method execute pada ThreadPool
yang bakal menerima parameter generik (generic parameter) bertipe F dengan
bounds berikut:
pub struct ThreadPool;
impl ThreadPool {
// --snip--
pub fn new(size: usize) -> ThreadPool {
ThreadPool
}
pub fn execute<F>(&self, f: F)
where
F: FnOnce() + Send + 'static,
{
}
}
Kita tetep pake tambahan () setelah trait FnOnce tersebut karena si FnOnce
ini merepresentasikan sebuah closure yang mana dia tidak menerima parameter
apa-apa dan dia juga mengembalikan unit type (tipe unit kosong) (). Sama kayak
halnya definisi fungsi (function definitions), tipe return-nya bisa
aja disingkirkan (omitted) dari signature-nya, tapi meskipun kita tidak
nerima parameter apa-apa di dalam closure-nya, kita tetep wajib nulisin tanda kurung
yang kosong tersebut (parentheses).
Sekali lagi, ini adalah sekadar implementasi paling simpel (simplest) dari method
execute: dia sama sekali tidak ngelakuin apa-apa, tapi kan tujuan kita cuma
mau ngebikin kode kita sukses di-compile doang buat saat ini. Mari kita cek (check) lagi deh:
$ cargo check
Checking hello v0.1.0 (file:///projects/hello)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.24s
Kompilasi sukses! (It compiles!) Tapi perlu dicatet nih kalau seandainya kita
mencoba ngejalanin pake cargo run dan terus nyoba ngasih sebuah request dari browser,
kita bakal ngelihat lagi error-error di browser tadi yang sempat kita lihat di
bagian awal bab ini. Library kita ini masih belum secara harfiah (actually) memanggil
closure yang dioper ke dalem fungsi execute lho ya!
Catatan: Pepatah yang mungkin sering kita dengar soal bahasa-bahasa pemrograman yang compiler-nya rewel (strict compilers), kayak Haskell dan Rust, adalah “kalau kodenya berhasil di-compile, berarti kodenya jalan.” Tapi pepatah ini itu tidak selalu bener kok. Project kita ini sukses di-compile kan, padahal dia itu bener-bener tidak ngelakuin apa-apa sama sekali! Kalau seandainya kita ini lagi ngebangun project sungguhan yang lengkap, ini adalah saat-saat yang paling pas buat mulai nulisin unit tests buat ngetes (check) apakah kodenya sukses di-compile sekaligus punya perilaku yang emang kita mau atau tidak.
Renungkan ini: kira-kira apa yang bakal berbeda di sini kalau seandainya kita ini lagi mau ngejalanin sebuah future ketimbang sebuah closure?
Memvalidasi Jumlah Threads yang Ada di new
Kita sama sekali tidak ngelakuin tindakan apa-apa lho sama parameter-parameter
yang ada di fungsi new dan execute ini. Mari kita implementasikan body
(isi) dari fungsi-fungsi ini supaya punya perilaku yang emang kita inginkan. Buat
memulai, mari kita pikirin soal fungsi new. Sebelumnya kita udah milih (chose) tipe
tanpa tanda (unsigned type) buat parameter size karena ngebikin sebuah pool dengan jumlah threads
yang negatif itu emang tidak masuk akal (makes no sense). Tapi ya,
sebuah pool dengan angka nol threads juga tidak kalah tidak masuk akalnya dong,
padahal angka nol itu adalah angka yang sah-sah aja (perfectly valid) di tipe
usize. Kita bakal tambahin barisan kode buat ngecek (check) kalau variabel size
itu harus lebih gede dari angka nol sebelum kita mengembalikan sebuah instance
ThreadPool lalu ngebikin programnya jadi panic dengan memakai macro
assert! kalau ternyata kita dikasih angka nol, kayak yang kelihatan di
Listing 21-13.
pub struct ThreadPool;
impl ThreadPool {
/// Create a new ThreadPool.
///
/// The size is the number of threads in the pool.
///
/// # Panics
///
/// The `new` function will panic if the size is zero.
pub fn new(size: usize) -> ThreadPool {
assert!(size > 0);
ThreadPool
}
// --snip--
pub fn execute<F>(&self, f: F)
where
F: FnOnce() + Send + 'static,
{
}
}
ThreadPool::new supaya dia panic kalau size-nya nolKita juga udah nambahin secuil dokumentasi buat ThreadPool kita memakai komentar
dok (doc comments). Perhatikan kalau kita udah mengikuti (followed) kaidah penulisan dokumentasi
yang bagus (good documentation practices) dengan cara nambahin sebuah seksi yang mana
membeberkan kasus-kasus (call out the situations) di mana fungsi kita ini bisa jadi
panic, kayak yang dibahas di Bab 14. Silakan cobain jalanin cargo doc --open
lalu klik struct ThreadPool tersebut buat ngelihat kayak apa rupa dari docs
yang udah di-generate buat method new tersebut!
Ketimbang nambahin macro assert! kayak yang baru aja kita lakuin di sini,
kita sebenernya bisa aja kok ngerubah method new ini jadi build dan terus
ngembaliin (return) sebuah tipe Result persis kayak apa yang udah kita lakuin
sama fungsi Config::build di dalem project I/O kita di Listing 12-9.
Tapi kita udah memutuskan kalau di kasus kali ini usaha buat ngebikin sebuah thread pool
tanpa punya satupun threads di dalamnya (without any threads) itu seharusnya
dijadikan sebuah error yang tidak bisa dipulihkan (unrecoverable error). Kalau
kita lagi ngerasa ambisius hari ini, cobain deh buat nulis sebuah fungsi bernama
build dengan signature berikut buat ngebandingin hasilnya dengan fungsi
new ini:
pub fn build(size: usize) -> Result<ThreadPool, PoolCreationError> {
Menyiapkan Tempat Buat Menyimpan Para Threads
Sekarang karena kita udah punya cara yang valid buat mengetahui (know) jumlah threads
yang harus disimpan di dalam pool, kita akhirnya bisa ngebikin threads tersebut lalu
menyimpan mereka di dalam struct ThreadPool sebelum kita ngembaliin si struct itu. Tapi
gimana ya caranya kita “menyimpan” sebuah thread? Mari kita ngelirik balik
(take another look) ke signature dari thread::spawn:
pub fn spawn<F, T>(f: F) -> JoinHandle<T>
where
F: FnOnce() -> T,
F: Send + 'static,
T: Send + 'static,
Fungsi spawn ini ngembaliin (returns) sebuah JoinHandle<T>, di mana T itu
adalah tipe balasan (return type) dari closure tersebut. Mari kita cobain pakai
JoinHandle juga deh dan lihat apa yang bakal kejadian. Di kasus yang kita kerjain
sekarang, closures yang kita oper masuk ke dalem thread pool ini emang tugasnya
buat nanganin (handle) koneksi dan bukannya buat ngembaliin data apa pun juga, jadi si
T ini nilainya bakal berupa unit type ().
Kode yang ada di Listing 21-14 ini bakal berhasil di-compile tapi masih belum ngebikin
satu pun threads. Kita udah ngubah (changed) definisi dari ThreadPool supaya dia
menyimpan (hold) sebuah vector yang isinya berupa instances dari
thread::JoinHandle<()>, menginisialisasi vector tersebut supaya punya kapasitas memori
sejumlah size (with a capacity of size), nyiapin (set up) loop for yang
nantinya bakal njalanin beberapa kode buat ngebikin threads tersebut,
dan baru deh membalikkan (returned) sebuah instance ThreadPool yang ngandung mereka semua.
use std::thread;
pub struct ThreadPool {
threads: Vec<thread::JoinHandle<()>>,
}
impl ThreadPool {
// --snip--
/// Create a new ThreadPool.
///
/// The size is the number of threads in the pool.
///
/// # Panics
///
/// The `new` function will panic if the size is zero.
pub fn new(size: usize) -> ThreadPool {
assert!(size > 0);
let mut threads = Vec::with_capacity(size);
for _ in 0..size {
// create some threads and store them in the vector
}
ThreadPool { threads }
}
// --snip--
pub fn execute<F>(&self, f: F)
where
F: FnOnce() + Send + 'static,
{
}
}
ThreadPool buat menampung para threads yang adaKita udah ngebawa (brought) std::thread masuk ke dalam scope di dalam library
crate kita gara-gara kita lagi mau makek thread::JoinHandle sebagai tipe buat
item-item yang bakal masuk ke dalam vector milik si ThreadPool.
Setelah angka size yang valid itu diterima (received), ThreadPool kita ini ngebikin
sebuah vector baru yang mana sanggup nampung sejumlah size item. Fungsi
with_capacity ini ngelakuin tugas yang sama persis kayak fungsi Vec::new
tapi dengan satu perbedaan krusial (important difference): dia udah mengalokasikan memori lebih dulu
(pre-allocates space) ke dalam vector tersebut. Karena kita tahu pasti (we know)
kalau kita ini perlu menyimpan size elemen di dalam vector ini, ngelakuin
proses alokasi (allocation) memori dari awal kayak gini itu slightly (sedikit)
lebih efisien dan cepet ketimbang makek fungsi Vec::new yang mana dia itu bakal
mengubah ukurannya sendiri (resizes itself) setiap kali ada elemen baru yang
dimasukin ke dalam situ.
Pas kita jalanin perintah cargo check lagi, dia harusnya berhasil (succeed).
Ngirimin Kode dari Dalam ThreadPool ke Sebuah Thread
Kita ninggalin (left) sebuah komentar di dalam loop for tadi yang ada di
Listing 21-14 mengenai (regarding) urusan pembuatan threads. Di sini, kita bakal
ngelihat gimana sebenarnya langkah yang kita tempuh buat ngebikin threads tersebut.
Standard library menyediakan fungsi thread::spawn sebagai cara buat bikin
threads baru, dan si thread::spawn ini ngeharepin buat langsung dikasih beberapa
kode yang harus dijalanin seketika (as soon as) pas thread tersebut selesai
dibikin. Padahal, di kasus yang kita punya, kita pengennya (want to)
ngebikin threads tersebut terus ngebikin mereka buat menunggu (wait) kode-kode
(tasks) yang bakal kita kirimin nanti (later). Implementasi (implementation) dari
threads bawaan (standard library) ini sama sekali tidak punya opsi (way) buat ngelakuin hal semacam
itu; jadinya kita harus mengimplementasikannya secara manual (manually).
Kita bakal mengimplementasikan perilaku (behavior) ini dengan cara
memperkenalkan sebuah struktur data baru (new data structure) di antara si
ThreadPool tersebut dan threads ini yang mana struktur data ini bakal bertugas
mengelola (manage) tingkah laku yang baru ini. Kita bakal namain struktur data ini
Worker (Pekerja), yang mana merupakan sebuah istilah lazim (common term) yang
suka dipakai di dalam berbagai implementasi model pemusatan (pooling). Sang Worker
ini tugasnya ngambilin (picks up) kode-kode (tasks) yang harus dijalanin terus
menjalanin kode-kode itu di dalam thread miliknya.
Coba bayangin (think of) kayak orang-orang yang lagi kerja di dalem dapur sebuah restoran: para pekerja dapur (workers) ini pada nungguin sampai pesanan (orders) datang dari para pelanggan (customers), dan terus mereka jadinya bertanggung jawab (responsible) buat nerima (taking) pesanan-pesanan tersebut terus menuhin (filling) pesanan itu (masak makanannya).
Ketimbang nyimpen vector yang isinya sekumpulan instances JoinHandle<()> di
dalam thread pool ini, kita sebaliknya bakal nyimpen instances dari struct
Worker. Masing-masing Worker ini bakal nge-store (menyimpan) sebuah instance
JoinHandle<()> tunggal. Kemudian kita bakal mengimplementasikan sebuah method pada
si Worker ini yang mana method itu nerima sebuah closure berisi kode yang harus
dijalanin terus method itu bakal ngirimin closure itu ke thread yang emang udah lagi pada nyala (already
running) supaya bisa tereksekusi. Kita juga bakal ngasih setiap Worker ini
sebuah id (identifikasi/identitas) supaya kita gampang mbedain di antara berbagai macam instances Worker
yang ada di dalam pool tersebut saat kita lagi nyatet log (logging) atau debugging.
Berikut ini adalah gambaran proses baru yang bakal berlangsung (happen) pas
kita ngebikin sebuah ThreadPool. Kita bakal mengimplementasikan kode yang tugasnya
ngirimin si closure tersebut ke thread itu setelah kita selesai nge-setup si Worker
ini memakai cara di bawah ini:
- Definisikan sebuah struct
Workeryang menampung (holds) sebuahiddan sebuahJoinHandle<()>. - Ubah
ThreadPoolsupaya dia itu sekarang malah nampung sebuah vector berisi instances dariWorker. - Definisikan sebuah fungsi
Worker::newyang nerima sebuah angka (number) buat jadiidterus dia ngembaliin sebuah instanceWorkeryang nampung siiditu beserta sebuah thread baru yang ditetaskan (spawned) memakai sebuah closure yang kosong. - Di dalam
ThreadPool::new, pakailah angka penghitung (counter) yang asalnya dari loopforitu buat di-generate (dijadiin) sebuahid, bikin sebuahWorkerbaru pakaiidtadi, terus masukin dan simpan siWorkerbaru itu ke dalam vector-nya.
Kalau kita ngerasa pengen nyari tantangan, coba deh kerjain sendiri perubahan-perubahan ini (implementing these changes on your own) sebelum kita ngelihat ke kodenya di Listing 21-15.
Udah siap (Ready)? Ini dia Listing 21-15 yang berisi salah satu cara buat ngebikin (make) serangkaian modifikasi-modifikasi sebelumnya.
use std::thread;
pub struct ThreadPool {
workers: Vec<Worker>,
}
impl ThreadPool {
// --snip--
/// Create a new ThreadPool.
///
/// The size is the number of threads in the pool.
///
/// # Panics
///
/// The `new` function will panic if the size is zero.
pub fn new(size: usize) -> ThreadPool {
assert!(size > 0);
let mut workers = Vec::with_capacity(size);
for id in 0..size {
workers.push(Worker::new(id));
}
ThreadPool { workers }
}
// --snip--
pub fn execute<F>(&self, f: F)
where
F: FnOnce() + Send + 'static,
{
}
}
struct Worker {
id: usize,
thread: thread::JoinHandle<()>,
}
impl Worker {
fn new(id: usize) -> Worker {
let thread = thread::spawn(|| {});
Worker { id, thread }
}
}
ThreadPool supaya dia menampung instances Worker ketimbang menampung threads secara langsungKita udah mengganti nama field di ThreadPool dari asalnya threads menjadi
workers karena emang sekarang dia jadinya malah nyimpen instances Worker
ketimbang instances dari JoinHandle<()>. Kita pakai angka hitungan
(counter) dari loop for tersebut sebagai argumen buat Worker::new, lalu
menyimpan (store) tiap Worker yang baru dibikin ke dalam vector yang namanya workers.
Kode-kode luar (external code, seperti server kita yang ada di src/main.rs) tidak perlu
tahu detail-detail spesifik implementasinya sehubungan sama pemakaian struct Worker
di dalam sebuah ThreadPool, makanya kita membiarkan struct Worker dan fungsi
new-nya itu bernilai (sifatnya) private. Fungsi Worker::new tersebut memakai
id yang udah kita kasih terus menyimpan sebuah instance dari JoinHandle<()> yang mana
instance ini dibikin dengan cara menelurkan sebuah thread baru memakai closure kosong.
Catatan: Kalau sistem operasinya (OS) tidak mampu buat membikin sebuah thread gara-gara kekurangan sumber daya sistem (aren’t enough system resources), fungsi
thread::spawnitu jadinya bakal meledak (panic). Hal ini bakal ngebikin seluruh server kita jadi ikutan panik, sekalipun pembuatan dari beberapa threads yang lain itu sebenarnya berhasil dengan lancar (might succeed). Demi urusan kemudahan buat dipelajari (simplicity’s sake), membiarkan kelakuan ini terjadi itu sah-sah saja kok (is fine), tapi kalau di kasus implementasi thread pool tipe tingkat produksi (production), kita bakal jauh lebih direkomendasiin (likely want to) buat memakaistd::thread::Builderbarengan sama methodspawn-nya karena method tersebut nge-return (ngembaliin) tipeResultsebagai gantinya.
Kode kita ini bakal bisa di-compile dan bakal berhasil menyimpan jumlah dari
instances Worker sebanyak yang udah kita spesifikasikan sebagai argumen
waktu manggil ThreadPool::new. Tapi kita ini masih juga belum memproses
(processing) closure yang kita dapetin dari pemanggilan execute ya. Mari kita ngelihat
gimana caranya supaya kita bisa ngerjain langkah yang itu sekarang.
Mengirim Requests ke Dalem Threads Melalui Channels (Saluran Komunikasi)
Permasalahan (problem) berikutnya yang harus segera kita tangani adalah fakta kalau closures yang dikasihin ke
thread::spawn itu bener-bener nyatanya tidak berbuat apa-apa (do absolutely nothing). Saat ini,
kita ngedapetin closure yang mana pengen kita jalanin tersebut (execute) lewat method execute.
Tapi kita ini perlu bisa ngasih si fungsi thread::spawn tadi sebuah closure untuk dijalankan
ketika (when) kita lagi repot-repotnya membikin setiap Worker tersebut saat fase-fase (during)
pembentukan (creation) dari ThreadPool itu.
Kita pengennya supaya struct-struct Worker yang baru aja kita bikin ini bisa ngambilin
(fetch) kode yang mau mereka jalanin tersebut yang dapetnya dari sebuah antrean (queue) yang
ditampung (held) di dalam ThreadPool terus ngirimin kode (task) tersebut ke thread
miliknya buat dijalankan.
Saluran komunikasi (channels) yang sempat kita pelajarin di Bab 16—sebuah
cara yang simpel buat berkomunikasi di antara dua buah threads—itu bakal jadi pilihan
(candidate) yang luar biasa pas sekali (perfect) buat menangani skenario (use case)
ini. Kita bakal memakai sebuah channel supaya dia bisa bertindak (function) sebagai antrean
pekerjaan (queue of jobs) tersebut, dan method execute bakal mengirimkan (send)
sebuah pekerjaan (job) dari dalam ThreadPool menuju instances Worker, yang
mana kemudian bakal ngirimin si job tersebut menuju thread miliknya. Ini
dia rancangannya (plan):
ThreadPoolbakal membikin (create) sebuah channel (saluran) dan berpegang erat (hold on to) pada bagian ujung pengirimnya (sender).- Masing-masing
Workerbakal berpegangan (hold on to) pada bagian penerimanya (receiver). - Kita bakal membikin sebuah struct
Jobbaru yang mana tugasnya buat nampung (hold) closures yang pengen kita kirimkan masuk menyusuri (down) si channel tersebut. - Method
executebakal mengirim (send) si job (pekerjaan) yang mau dia eksekusi (execute) tersebut melalui si sender (pengirim) ini. - Di dalam thread masing-masing, si
Workerini bakal muter berulang-ulang (loop over) menanyai receiver-nya (penerimanya) lalu mengeksekusi closures yang asalnya dari semua jobs apa pun yang mana dia terima (receive).
Mari kita mulai (start by) dengan ngebikin sebuah channel di dalam ThreadPool::new
dan menyimpan (holding) si pengirim (sender) itu di dalam instance
ThreadPool kita ini, persis kayak apa yang ditunjukin di Listing 21-16. Struct Job
ini sendiri belum nampung benda apa pun sih buat saat ini tapi si Job inilah yang bakal jadi
tipe dari item yang lagi mau kita kirim menyusuri (down) ke dalam channel (saluran) ini.
use std::{sync::mpsc, thread};
pub struct ThreadPool {
workers: Vec<Worker>,
sender: mpsc::Sender<Job>,
}
struct Job;
impl ThreadPool {
// --snip--
/// Create a new ThreadPool.
///
/// The size is the number of threads in the pool.
///
/// # Panics
///
/// The `new` function will panic if the size is zero.
pub fn new(size: usize) -> ThreadPool {
assert!(size > 0);
let (sender, receiver) = mpsc::channel();
let mut workers = Vec::with_capacity(size);
for id in 0..size {
workers.push(Worker::new(id));
}
ThreadPool { workers, sender }
}
// --snip--
pub fn execute<F>(&self, f: F)
where
F: FnOnce() + Send + 'static,
{
}
}
struct Worker {
id: usize,
thread: thread::JoinHandle<()>,
}
impl Worker {
fn new(id: usize) -> Worker {
let thread = thread::spawn(|| {});
Worker { id, thread }
}
}
ThreadPool buat menyimpan sender dari sebuah channel yang mentransmisikan instances JobDi dalam ThreadPool::new, kita membikin channel yang baru terus nyuruh si
pool tersebut buat menyimpen (hold) ujung si pengirimnya (sender). Kode ini
bakal berhasil sukses di-compile dengan mulus.
Mari kita cobain buat ngoper masuk sebuah ujung penerima (receiver) dari si channel ini
ke masing-masing Worker seiringan saat si thread pool lagi ngebikin si channel
tersebut. Kita tahu (know) kan kalau kita itu pengen makek bagian receiver (penerima) ini
dari dalam thread yang mana ditetaskan (spawn) oleh tiap instances si Worker tadi, jadi kita
bakal memasukkan referensi (reference) parameter si receiver ini di dalam closure yang
lagi kita buat itu. Sayangnya (won’t quite), kode yang ada di Listing 21-17 ini ternyata masih belum
bisa di-compile dengan sukses nih buat saat ini.
use std::{sync::mpsc, thread};
pub struct ThreadPool {
workers: Vec<Worker>,
sender: mpsc::Sender<Job>,
}
struct Job;
impl ThreadPool {
// --snip--
/// Create a new ThreadPool.
///
/// The size is the number of threads in the pool.
///
/// # Panics
///
/// The `new` function will panic if the size is zero.
pub fn new(size: usize) -> ThreadPool {
assert!(size > 0);
let (sender, receiver) = mpsc::channel();
let mut workers = Vec::with_capacity(size);
for id in 0..size {
workers.push(Worker::new(id, receiver));
}
ThreadPool { workers, sender }
}
// --snip--
pub fn execute<F>(&self, f: F)
where
F: FnOnce() + Send + 'static,
{
}
}
// --snip--
struct Worker {
id: usize,
thread: thread::JoinHandle<()>,
}
impl Worker {
fn new(id: usize, receiver: mpsc::Receiver<Job>) -> Worker {
let thread = thread::spawn(|| {
receiver;
});
Worker { id, thread }
}
}
WorkerKita udah membuat sedikit perubahan kecil dan gampang (straightforward): kita oper si receiver
masuk ke dalam Worker::new, terus kita memakai si receiver itu di dalam
closure-nya.
Pas kita mencoba buat ngecek kode ini (check this code), kita langsung kejedot sama error ini nih:
$ cargo check
Checking hello v0.1.0 (file:///projects/hello)
error[E0382]: use of moved value: `receiver`
--> src/lib.rs:26:42
|
21 | let (sender, receiver) = mpsc::channel();
| -------- move occurs because `receiver` has type `std::sync::mpsc::Receiver<Job>`, which does not implement the `Copy` trait
...
25 | for id in 0..size {
| ----------------- inside of this loop
26 | workers.push(Worker::new(id, receiver));
| ^^^^^^^^ value moved here, in previous iteration of loop
|
note: consider changing this parameter type in method `new` to borrow instead if owning the value isn't necessary
--> src/lib.rs:47:33
|
47 | fn new(id: usize, receiver: mpsc::Receiver<Job>) -> Worker {
| --- in this method ^^^^^^^^^^^^^^^^^^^ this parameter takes ownership of the value
help: consider moving the expression out of the loop so it is only moved once
|
25 ~ let mut value = Worker::new(id, receiver);
26 ~ for id in 0..size {
27 ~ workers.push(value);
|
For more information about this error, try `rustc --explain E0382`.
error: could not compile `hello` (lib) due to 1 previous error
Kodenya ini lagi berusaha (trying to) ngoper nilai receiver yang cuma satu ini ke
banyak instances Worker secara bebarengan. Ini jelas tidak bisa jalan (won’t work), seperti
yang pasti kita masih ingat (recall) dari memori kita pas lagi ngebaca Bab 16: bentuk implementasi
channel yang disediain sama Rust itu formatnya adalah sistem banyak pengirim (multiple producer),
tapi cuma satu penerima (single consumer). Ini bermakna kalau kita ini tidak bisa lho sekadar nge-clone
(kloning) si bagian consumer (pengkonsumsi) dari saluran komunikasi ini buat mbetulin kode
yang lagi eror ini. Lagian, kita emang sebenernya juga tidak mau kok (don’t want to) buat
ngirimin pesan yang itu-itu lagi berkali-kali nuju ke bermacam consumers (penerima pesan);
kita sebaliknya pengen ngirimin satu list panjang isinya pesan ke banyak (multiple) instances
Worker tapi dengan harapan bahwa masing-masing pesannya itu cuma berhak (gets processed)
bakal diproses sekali doang secara giliran.
Sebagai tambahan (additionally), tindakan ngambil (taking) satu job pekerjaan ngelepasin
dari queue (antrian) saluran tersebut itu emang pastinya bakal mengubah wujud (mutating)
si receiver-nya ini, gara-gara hal ini makanya threads yang ada itu sangat perlu punya sebuah
mekanisme (way) yang dirasa aman (safe) supaya mereka bisa bagi-bagi (share) dan ngubah
(modify) isi receiver ini secara bebarengan; kalau tidak, kita malah bisa-bisa ngejeblos dapet
race conditions (perlombaan data) yang bikin program rusak (seperti yang udah dibahas babak
belur di Bab 16 kemaren).
Ingat balik soal tipe smart pointers (pointer cerdas) yang udah terjamin aman buat di
dalam thread (thread-safe) yang barusan kita obrolin di Bab 16 kemaren: buat membagikan
(share) hak kepemilikan (ownership) melintasi (across) banyak threads yang berbeda dan secara bebarengan
ngebolehin para threads tersebut buat ngubah (mutate) nilai datanya bareng-bareng, kita
sangat butuh pake Arc<Mutex<T>>. Tipe Arc ini yang bakal ngebolehin kalau banyak instances
Worker buat bisa sama-sama ngantongin (own) si receiver, dan tipe Mutex ini yang bakal
mastiin ke kita kalau di satu titik waktu tertentu (at a time) itu cuma ada bener-bener
satu doang dari sekian banyak Worker yang berhak ngambil sebuah job (kerjaan)
langsung dari si receiver (penerimanya) tersebut. Listing 21-18 di bawah ini mendemonstrasikan perubahan
macam apa yang wajib kita lakuin ini.
use std::{
sync::{Arc, Mutex, mpsc},
thread,
};
// --snip--
pub struct ThreadPool {
workers: Vec<Worker>,
sender: mpsc::Sender<Job>,
}
struct Job;
impl ThreadPool {
// --snip--
/// Create a new ThreadPool.
///
/// The size is the number of threads in the pool.
///
/// # Panics
///
/// The `new` function will panic if the size is zero.
pub fn new(size: usize) -> ThreadPool {
assert!(size > 0);
let (sender, receiver) = mpsc::channel();
let receiver = Arc::new(Mutex::new(receiver));
let mut workers = Vec::with_capacity(size);
for id in 0..size {
workers.push(Worker::new(id, Arc::clone(&receiver)));
}
ThreadPool { workers, sender }
}
// --snip--
pub fn execute<F>(&self, f: F)
where
F: FnOnce() + Send + 'static,
{
}
}
// --snip--
struct Worker {
id: usize,
thread: thread::JoinHandle<()>,
}
impl Worker {
fn new(id: usize, receiver: Arc<Mutex<mpsc::Receiver<Job>>>) -> Worker {
// --snip--
let thread = thread::spawn(|| {
receiver;
});
Worker { id, thread }
}
}
Worker tersebut dengan cara makek Arc dan MutexDi dalam fungsi ThreadPool::new, kita ngeletakkin (put) si receiver tersebut ke dalam balutan
sebuah Arc dan sebuah Mutex. Buat masing-masing Worker yang baru dibikin (new), kita meng-clone
(bikin kloningan) si Arc ini tujuannya supaya dia bakal nge-bump (nambah) reference count-nya (hitungan referensinya)
sedemikian rupa sehingga keseluruhan instances dari Worker ini pada akhirnya bisa
saling ngebagi-bagi hak kepemilikannya bareng (share ownership) buat si penerima (receiver) tersebut.
Dengan berbekal semua perubahan ini, kodenya akhirnya bisa sukses di-compile! Kita udah hampir nyampe (getting there) ke tujuan akhir kita ini loh!
Mengimplementasikan Method execute
Mari kita benar-benar akhirnya mulai ngerjain dan mengimplementasikan method execute
pada ThreadPool tersebut secara tuntas. Kita juga bakal ngubah tipe Job ini dari asalnya sebuah struct menjadi
sebuah type alias (alias buat sebuah tipe) aja buat nampung si trait object (objek trait) yang mana
benar-benar bakal nampung (hold) secara bener tipe dari closure asli yang mana si execute ini tadi lagi nerima.
Kayak yang barusan kelar dibahas di “Membikin Sinonim Tipe dengan Type Aliases”
di dalem Bab 20 kemaren, fitur type aliases ini ngebolehin kita buat mempersingkat (make shorter)
nama dari tipe-tipe yang kelihatannya sumpek kepanjangan supaya nanti mereka itu jadi jauh lebih
enak plus lebih gampang (ease) buat dipakek sehari-hari. Coba tengok dan perhatikan Listing 21-19
ini deh.
use std::{
sync::{Arc, Mutex, mpsc},
thread,
};
pub struct ThreadPool {
workers: Vec<Worker>,
sender: mpsc::Sender<Job>,
}
// --snip--
type Job = Box<dyn FnOnce() + Send + 'static>;
impl ThreadPool {
// --snip--
/// Create a new ThreadPool.
///
/// The size is the number of threads in the pool.
///
/// # Panics
///
/// The `new` function will panic if the size is zero.
pub fn new(size: usize) -> ThreadPool {
assert!(size > 0);
let (sender, receiver) = mpsc::channel();
let receiver = Arc::new(Mutex::new(receiver));
let mut workers = Vec::with_capacity(size);
for id in 0..size {
workers.push(Worker::new(id, Arc::clone(&receiver)));
}
ThreadPool { workers, sender }
}
pub fn execute<F>(&self, f: F)
where
F: FnOnce() + Send + 'static,
{
let job = Box::new(f);
self.sender.send(job).unwrap();
}
}
// --snip--
struct Worker {
id: usize,
thread: thread::JoinHandle<()>,
}
impl Worker {
fn new(id: usize, receiver: Arc<Mutex<mpsc::Receiver<Job>>>) -> Worker {
let thread = thread::spawn(|| {
receiver;
});
Worker { id, thread }
}
}
Job supaya nampung sebuah Box yang aslinya berisi masing-masing closure dan terus ngirimin si job ini menelusuri ke dalem channelnyaSehabis kita selesai kelar bikin (creating) instance dari sebuah Job yang baru yang
berbekalkan closure yang kebetulan kita raup (get) dan kita panen dari parameter di fungsi execute ini,
kita akhirnya ngirimin si job ini merosot lurus menuju saluran (channel) komunikasi nglewatin sending end
(ujung buat ngirim) si sender ini. Di situ kita itu emang manggil unwrap pas lagi manggil method send buat
antisipasi seandainya pengirimannya itu entah kenapa jadi gagal (fails). Hal macam error ini
sebenernya emang masih ada peluang kejadian (might happen), apalagi kalau misalnya contohnya,
kita itu nyetop secara paksa (stop) biar semua threads kita ini berhenti melakukan semua pekerjaan dan ngejalanin eksekusinya,
yang mana artinya si ujung penerimanya (receiving end) tersebut udah pasti juga ikutan
mandeg (stopped) alias udah tidak nerima pesen (messages) baru lagi. Sampai dengan menit saat
ini sih, emang jujurnya kita ini masih belum nyiapin fitur apa pun buat bisa
berhentiin (stop) segerombolan threads ini dari ngejalanin eksekusi programnya: threads
yang udah jalan milik kita ini masih bakal terus melenggang jalan narik ngegas pol gas terus beroperasi
(continue executing) sekuat lama umur (as long as) pool milik kita ini juga masih dibiarin buat tetep exist.
Satu-satunya alasan kenapa kita dengan berani masang (use) fitur unwrap di situ adalah karena
berbekal jaminan (know) dari diri kita sendiri kalau skenario kemungkinan gagalnya (failure case) ini sebenernya emang
benar-benar secara harfiah tidak bakal pernah terwujud kejadian sama sekali, tapi ya sayangnya si
compiler ini mana ngerti kalau di kenyataannya kelakuan ini itu tidak bakal pernah berbuat salah macam itu (doesn’t know that).
Tapi kita ini juga belum benar-benar beres juga nih kerjaannya! Di dalam bagian Worker itu, si closure
kepunyaan kita ini yang tadinya dioper ke dalem thread::spawn kan emang tugas utamanya dia itu cuma
lagi ngerujuk (meminjam referensi/references) doang kan ke si ujung penerima (receiving end) milik si saluran itu.
Padahal yang bener-bener kita harapkan di sini adalah, kita ngebutuhin sekali supaya
si closure ini benar-benar bisa berputar dan berkeliling di dalam loop buat selama-lamanya (forever),
nanyain dan malakin (asking) si ujung penerima channel ini mulu nanyain apaan dia ini lagi dapet kerjaan job
sambil setelahnya dia benar-benar ngejalanin isi dari si pekerjaan (running the job) ini langsung abis dia kebetulan berhasil
ngedapetin satu job. Makanya, mari kita lakuin rombakan perubahan barusan yang ada kelihatan nyempil di
Listing 21-20 tersebut ke dalam dalem fungsi Worker::new ini.
use std::{
sync::{Arc, Mutex, mpsc},
thread,
};
pub struct ThreadPool {
workers: Vec<Worker>,
sender: mpsc::Sender<Job>,
}
type Job = Box<dyn FnOnce() + Send + 'static>;
impl ThreadPool {
/// Create a new ThreadPool.
///
/// The size is the number of threads in the pool.
///
/// # Panics
///
/// The `new` function will panic if the size is zero.
pub fn new(size: usize) -> ThreadPool {
assert!(size > 0);
let (sender, receiver) = mpsc::channel();
let receiver = Arc::new(Mutex::new(receiver));
let mut workers = Vec::with_capacity(size);
for id in 0..size {
workers.push(Worker::new(id, Arc::clone(&receiver)));
}
ThreadPool { workers, sender }
}
pub fn execute<F>(&self, f: F)
where
F: FnOnce() + Send + 'static,
{
let job = Box::new(f);
self.sender.send(job).unwrap();
}
}
struct Worker {
id: usize,
thread: thread::JoinHandle<()>,
}
// --snip--
impl Worker {
fn new(id: usize, receiver: Arc<Mutex<mpsc::Receiver<Job>>>) -> Worker {
let thread = thread::spawn(move || {
loop {
let job = receiver.lock().unwrap().recv().unwrap();
println!("Worker {id} got a job; executing.");
job();
}
});
Worker { id, thread }
}
}
Worker tersebutDi sini, hal pertama yang kita lakuin duluan itu adalah manggil lock dari si receiver
tadi tujuannya buat mengakuisisi atau merebut dan narik status si mutex ini, dan
baru abis narik status aman itu barulah kita berani manggil unwrap biar nantinya kodenya bisa otomatis njerit panik (panic)
atas segala bentukan macam rupa kesalahan (errors) yang mungkin keluar. Merebut (Acquiring) status kunci
(lock) itu pada aslinya sangat mungkin berpeluang buat jadi gagal berantakan kalau ternyata di balik bayangan layar,
kondisi status (state) dari dalem the mutex-nya ini udah keburu kejebak berubah menjadi beracun keracunan (poisoned state).
Kondisi beracun ini bisa terjadi kalau ternyata ada utas (thread) yang lainnya yang entah
salah apa dari mana dia tiba-tiba malahan udah milih panik meledak berantakan (panicked) tapi saat itu dia ini masih ngantongin status
kuncinya (holding the lock) ini alih-alih melepaskan kunci tersebut (releasing the lock) sebelum matinya.
Kalau kita lagi apes kejebak situasi (situation) yang semacam begini, maka tindakan manggil method unwrap biar si utasan (thread) kita
yang ini juga ikut-ikutan njerit panik ikutan hancur adalah langkah jalur yang emang diakui emang udah bener (correct action)
buat dikerjain. Silakan santai aja luangin bebasin dan rombak sendiri ini semua ngubah (change) bentuk unwrap ini
biar jadinya makek format dari expect yang disisipin makek pesan galat kesalahan (error message)
yang mungkin agak lebih masuk kerasa ngena bunyinya gampang dimengerti sama kuping (meaningful to you)
sendiri aja tidak masalah.
Kalau ternyata kita mulus-mulus aja sukses mulus kebagian ngunci gembok (got the lock) ke gembok mutex ini,
maka baru di saat itu kita bisa ikutan berani nekat manggil si perintah recv biar bisa nangkep nerima (receive) satu balok Job (Pekerjaan) dari si channel saluran komunikasi pipa tersebut.
Sebuah sematan paku tempelan unwrap yang nangkring di bagian pucuk paling terakhir ini
bener-bener membantu kita berjalan tegar dan melewati (moves past) nerjang segala bentuk halangan segala kelakuan aneh error macam apa aja yang
kebetulan mungkin aja tetiba nongol dari sini, error model macem ginian bisa aja pada nongol mencuat (occur) umpamanya di mana si utas thread
yang mana lagi sibuk-sibuknya naruh (holding) megangin ujung sender-nya ini ternyata tetiba udah keburu modar matikan paksa nutup jalan (shut down) ngedahuluin si penerimanya.
Yang ini sebenernya jalan nalar kerjanya emang nyerempet mirip-mirip (similar) nian loh percis dengan cara
fungsi metode send yang mana bakal pasti berontak ngebalik (returns) pesan Err semisal si utas sang receiver-nya yang ini malah mati berantakan nutup mendadak dari depan.
Panggilan metode lurus menuju si baris recv ini sifatnya membikin laju proses berhentinya kodenya jadi terblokir nunggu (blocks), makanya ini
menimbulkan efek di mana kalau andai kata aja ternyata eh emang belum nongol dateng kerjaan job satu biji doang pun di depan matanya saat itu (no job yet), maka di ujung akhir-akhirnya ini sih utas *thread yang lagi kerja saat ini (current thread) jadinya ya mau gimanapun bakal terus bengong nganggur nungguin nge-drop nunggu nunggu anteng (wait) sampe sebuah job bener-bener kelar udah nyampe hadir keluar di depannya. Struct si Mutex<T> pada intinya inilah si sosok kuncian yang menjamin mastiin pasti (ensures) ke semuanya ke kita kalau emang disisihkan pada sebuah jeda satu waktu yang spesifik
tertentu (at a time) bener-bener bakal pasti cuma bakal disidang dan diberika akses ke cuma satu doang thread punya Worker (only one Worker thread) yang dikasih dan dibolehin ijin buat nyoba manggil-manggil mau request nanyain apaan ada job (pekerjaan)
atau gaknya ke channel tadi.
Tadaa, selamat loh! Thread pool bikinan kita pada detik jaman tulisan ini dibikin sebenernya udah pada posisi sukses jalan ngacir lanjay ngebut dengan mulus (working state)!
Majuin cobain aja suruh tes dengan nge-running cargo run sekalian lu buat dikit requests ngetes jalannya juga ya:
$ cargo run
Compiling hello v0.1.0 (file:///projects/hello)
warning: field `workers` is never read
--> src/lib.rs:7:5
|
6 | pub struct ThreadPool {
| ---------- field in this struct
7 | workers: Vec<Worker>,
| ^^^^^^^
|
= note: `#[warn(dead_code)]` on by default
warning: fields `id` and `thread` are never read
--> src/lib.rs:48:5
|
47 | struct Worker {
| ------ fields in this struct
48 | id: usize,
| ^^
49 | thread: thread::JoinHandle<()>,
| ^^^^^^
warning: `hello` (lib) generated 2 warnings
Finished `dev` profile [unoptimized + debuginfo] target(s) in 4.91s
Running `target/debug/hello`
Worker 0 got a job; executing.
Worker 2 got a job; executing.
Worker 1 got a job; executing.
Worker 3 got a job; executing.
Worker 0 got a job; executing.
Worker 2 got a job; executing.
Worker 1 got a job; executing.
Worker 3 got a job; executing.
Worker 0 got a job; executing.
Worker 2 got a job; executing.
Sukses mulus (Success)! Kita sekarang ngenalin kalau kita emang akhirnya sukses benar-benar punyain sebuah bentuk sejati dari sebuah thread pool murni yang mengeksekusi koneksi-koneksi yang singgah dari depan ini ngejalanin proses eksekusinya berbarengan asinkron tidak usah gantian satu-satu lgi secara sinkron (asynchronously). Emang kenyataannya kagak bakal pernah (never) ada riwayat kejadian di mana lebih dari jumlah sekian empet (four) potong threads ini di-lahirkan (created) pas lagi ngejalanin kode di depannya ini. Jaminan ini ngegaransi ke seisi segenap bangsa sistem (our system) kita ke semuanya tidak bakal pernah sekalipun ikutan panik mabuk jadi kepanasan (overloaded) bahkan bilamana semisal jikalau aja mesin server kita itu dikeroyok dikirim hujan dideras rentetan gelombang panah ngerima request super bertubi-tubi seabreg rupa yang pada berjejer dateng masuk (receives a lot of requests). Andaikata emang bener lho kita sampe sengaja jahil iseng-iseng kita nge-request secara spesifik (make a request) ke alamat lintasan jalur (path) tujuan kita ini di /sleep, maka server kita yang satu ini emang bakal teteupan tetep tegak berdiri bisa santai terus dengan gampangnya bisa ngelayanin ngeresponi (serve) panggilan para rentetan requests yang lainnya gara-gara dia tinggal nyuruh thread Worker lain yang emang dari kemaren emang nganggur buat sekalian ikutan terjun ngerjain bantu manggil fungsi tasks tersebut.
Catatan: Andai kata lu maksa nekat ngebuka path rute link URL tujuan yang ditaruh di dalem rute khusus jebakan si /sleep ini di dalem banyakan (multiple) jajaran tumpukan sekian puluhan (multiple) browser windows secara bareng-bareng langsung sekaligus jebred dalam waktu sekerdipan mata bebarengan (simultaneously), kita mungkina aja kelewatan bingung kepancing ngerasa panik dan nyangka kok kelihatannya dia ngeladenin ngememuat nge-load-nya ganti-gantian dengan selang pelan durasi santai (time intervals) per five-second (selang lima deik) sekali padahal kita udah makek pool multithread. Aslinya benar-benar sebagian (some) tipe dari program jenis peramban web browsers jaman di luar sekarang (today) emang emang punya perilaku ngeksekusi kelakuan ganjil ngerapihin nge-barisin numpuk antrian deretan dari satu deretan requests-nya yang asalnya identik dan modelnya bener-bener punya endpoint rute kembar sama persis ganjil secara sengaja satu per satu berurut bergantian (sequentially), ya murni semata-mata itu gara-gara (reasons) sekadar buat tujuan urusan nge-caching. Intinya pembatasan kebodohan macem (limitation) begini ini mah samsek seutuhnya tidak diakibatin bersumber murni disebabkan (not caused by) ulah dari kelakuan kinerja web server yang kita lagi pada bikin ini.
Saat-saat momen sekarang yang ini emang kelihatannya pas rasanya emang waktu jeda yang enak (good time)
buat minggir mingser ambil napas ngaso sebentar ngecoba ngebayangin nyimak (pause and consider) ngebandingin
gimanakah jeroannya raut muka model rancangan tatanan bentukan kode-kodean barisan yang pada nangkring mentereng
sedari di dalem bingkai bingkisan kotak Listings 21-18, 21-19, dan ujungnya juga di selipan 21-20 ini
seumpamanya bentuk mereka itu dibikin pada agak berbeda sedikit andaikata kta beralih ngeganti jalan pikir buat emang lebih
memilih (using) tatanan asinkron bernama futures ketimbang bertahan kukuh cuma memakai fungsional model closure doang buat emang
membedah ngerjain mengeksekusi semua rupa tugas (work) tersebut. Bagian tipe-tipe spesifik manakah (what types) di sana yang pada
bakal ngalamin ubah ganti baju (would change)? Kayak manakah (how) emangnya signatures aslinya dari method (method signatures)-nya ini
kudu ikutan berubah drastis diganti-ganti mingser ke sana-kemari, seandainya emang emang perlu perombakan bener-bener (if at all)? Sisi pinggiran belah pecahan (parts of the code) manakah sih
yang bakal tetep dibiarin aman bertahan (stay) anteng kagak perlu ikutan ngalamin diobok-obok perubahan (the same)?
Abis panjang lebar berpuas-puas dirimu beres nuntasin masa masa ngulik ngebaca bahan di pelajaran soal tata krama kelakuan dari jenis gubahan struktur kelakuan loop dari bentuk model while let ini yang kita kulik balik pas ngerampungin bab-bab awal di antara Bab 17 dan sembari selip di tengah-tengahnya Bab 19 pula, otak di kepala dirimu barangkali mulai berbisik kepikiran lari membatin (you might be wondering) kenapa yak pantesan kok kita nggak langsung main sabet milih aja buat mutusin secara kilat nekat naruh masang serangkaian urutan kode eksekusi milik utas thread khusus si Worker kita (Worker thread code) yang seolah nampak rupa cantiknya kayak yang sempet ngintip kepajang di dalem pajangan galeri Listing 21-21.
use std::{
sync::{Arc, Mutex, mpsc},
thread,
};
pub struct ThreadPool {
workers: Vec<Worker>,
sender: mpsc::Sender<Job>,
}
type Job = Box<dyn FnOnce() + Send + 'static>;
impl ThreadPool {
/// Create a new ThreadPool.
///
/// The size is the number of threads in the pool.
///
/// # Panics
///
/// The `new` function will panic if the size is zero.
pub fn new(size: usize) -> ThreadPool {
assert!(size > 0);
let (sender, receiver) = mpsc::channel();
let receiver = Arc::new(Mutex::new(receiver));
let mut workers = Vec::with_capacity(size);
for id in 0..size {
workers.push(Worker::new(id, Arc::clone(&receiver)));
}
ThreadPool { workers, sender }
}
pub fn execute<F>(&self, f: F)
where
F: FnOnce() + Send + 'static,
{
let job = Box::new(f);
self.sender.send(job).unwrap();
}
}
struct Worker {
id: usize,
thread: thread::JoinHandle<()>,
}
// --snip--
impl Worker {
fn new(id: usize, receiver: Arc<Mutex<mpsc::Receiver<Job>>>) -> Worker {
let thread = thread::spawn(move || {
while let Ok(job) = receiver.lock().unwrap().recv() {
println!("Worker {id} got a job; executing.");
job();
}
});
Worker { id, thread }
}
}
Worker::new yang coba-coba ngandelin while letJujur emang barisan rombongan susunan set perabot alat tempur kode-kodean ini mah emang aslinya (this code compiles and runs) pantes sukses bisa di-compile lalu berhasil sukses diajak buat running nyala-nyala aja aslinya asalkan dipanggil mah tanpa cacat, akan tetapi (but) sayangnya kelakuannya dia tidak bakal sanggup membikin lalu menyetorkan membuahkan asilnya tingkah prilaku eksekusi pola ngantri tata perlintasan multi threading yang aslinya sembenernya emang jujur jadi inceran idam-idaman (desired threading behavior) sasaran awal milik hajat (desired) diri kita: gara-gara ujung-ujungnya ya ntar sebuah request yang super lelet lambat (a slow request) jalannya bakal cuman ngehasilkan requests yang berentet di antrean lain di belakangnya yang tetep ngadat mandek mandeg antri merana kudu terpaksa harus disuruh wait menunggu dan antri ngurut memanjang barisan kebagian jatah jadwal biar mereka bisa dieksekusi dikelarin dibereskan dan diproses (processed). Perkara alasan dibalik reason kejanggalan ginian ini tergolong ada sedari kelakuan halus subtle: bahwa sesungguhnya susunan wujud si struct Mutex ini emang takditakdirkan emang tidak dikasih public method (metode publik) khusus sengaja dikasih nama panggilan buat fitur nge-unlock karena nyatanya status penguasa kunci miliknya kepemilikan jatah buat kuncian rahasia tersebut ini aslinya udah emang based on terikat mutlak dikunci ditentuin dari seberapa panjang seberapa bentar siklus riwayat hidup umur lifetime umurnya si benda keramat bertipe perlindungan yang menyandang identitas MutexGuard<T> yang mana sosok tersebut disisipkan rapi di perut selimut isi LockResult<MutexGuardlock buat dia setorkan ngeluarin dia (returns) pas selesai beres dia dipanggil beroperasi jalan. Menjelang masa kompilasi (at compile time), sosok mandor tukang sensor pinjeman borrow checker inilah yang selanjutnya ganti berjaga bakal dengan telitinya bisa memelototi lalu memberlakukan narapain menegakkan menembakkan enforce the rule maklumat seputar tata kaidah yang ngegarisbawahi kalau setiap butir selongsong resource (sumber daya) yang tadinya ketat diselimuti diborgol ketat (guarded) diawasi pelindungan di belakang benteng pelindung sebuah palang gembok berjenis Mutex itu niscaya haram statusnya diharamkan (cannot be accessed) sama sekali dilarang terjamah alias tidak bakal mempan dibolehin buat bisa diutak-atik (diakses) oleh siapapun pun kalau seumpamanya status kita saat di kejadian eksekusi saat itu kondisinya lagi belom emang kita megang lalu punya si lock stempel kuncinya ini di tangan (unless we hold the lock). Masalahnya adalah, penerapan (implementation) kode yang bergaya ngasal semacam ini berisiko nimbul-nimbulin bahaya efek di mana wujud kuncian (lock) dari yang mestinya diserahin kembali ini ternyata masih dipaksa kudu bertahan kepeluk tertahan kepegang erat-erat tertahan di genggaman (being held) jauh memakan durasi yang kepanjangan sangat jauh di luar dari maksud jadwal normal target awal selesainya tugas aslinya seandainya (intended if we aren’t mindful) andaikata otak pikiran kita belom ngerasa gih hati-hati nyadari seputaran mindful ngeh dan perduli buat memandang mikirin masalah masa waktu jeda lifetime (lama waktu) dari MutexGuard
Sepetak bentuk kodingan susunan naskah dari balok kode di balikan dalem ruang gubahan Listing 21-20 yang menumpukan tumpuannya di dalam bentuk susunan gaya pemakaian dari format pemanggilan dari perkenalan lajur baris berupa panggilan let job = receiver.lock().unwrap().recv().unwrap(); benar-benar jalan bekerja karena aslinya dengan membiasakan manggil (with let), rupa jenis semua ragam dari kumpulan serpihan serentetan aneka serabut serangkaian sosok rentetan sekian harga biji temporary values nilai-nilai angka dadakan instan bayangan yang kebetulan aja sementara disisipin (temporary values) dipakai sengaja dipakai di daleman seonggok kumpulan bongkahan expression tersebut yang ada bertumpuk mojok mentereng ngendon mangkal terparkir mejeng di perbatasan tapal batas perlintasan di ruas sisi lajur paling right-hand side pojokan sebelah barisan pinggiran sisi seberang belahan sisi arah sebelah kanannya dari batasan silang palang lintasan lambang sama dengan (the equal sign) bakal emang nasibnya dengan sadar seketika cepatnya dalam detik waktu itu juga (immediately dropped) dimusnahin diputus di-drop hilang tak bersisa ditiadakan pas waktu (when) persis saat barisan deretan panggilan milik sang let statement ini nemuin garis ajalnya dan kelar nyampe berakhir tamat ngakhirin garis ends nasib eksekusinya. Beda malang apes nasib halnya, si konstruksi kelakuan si panggilan buat while let (dan tak pelak kelakuan yang kembar sama juga melekat pada panggilan buat deretan panggilan di struktur perlintasan buat konstruksi barisan kelakuan if let dan juga konstruksi yang ngerujuk seputaran kelakuan match) sifat kodrat aslinya pantang dan sejatinya anti dan juga (does not) tidak pernah sekalipun membuang secara sepihak memusnahkan (drop) barang bawaannya beruba wujud barang rakitan nilai-nilai aneka rentetan sosok figur temporary values angka naskah serpihan bawaan serba sementaranya (temporary values) sisaan tersebut sebelum rentetan eksekusinya ini benar-benar merayap tuntas tiba berjalan sampai ngejejak sukses nutup berhasil (until the end of) merambat tiba menapaki penghujung purna tutup gawang palang blok kodenya (the associated block). Merujuk narasi skenario paparan kejadian malang perbandingan percontohan yang tergambar pas nongkrong mojok di sela daleman sketsa yang terbingkai rapi di sela Listing 21-21 ini tadi, sosok batang kuncian yang ditugasin gembok gerbang (lock) tetep ajeg kepaksa bakal awet mangkal terkunci (remains held) nangkring mengunci selagi sepanjang (for the duration of) di sepanjang rentetan durasi berlangsungnya kelakuan waktu (the call to) lamanya rentang periode pas fungsi tugas sang job() tersebut lagi ngejalanin rutinitas gawang di dalam masa rutinitasnya buat menunaikan (call to) ngejalanin seisi jeroan badan pemanggilan tugas kerjanya tsb., niscaya memunculkan satu hal arti rupa artian yang maknanya adalah emang pada saat detik-detik nasib gembok lagi kehalang nutup rupa begini maka segenap sekompi pasukan komplotan deretan gerombolan Worker instances yang other (para punggawa lainnya) bakal pastinya pada terpojok tak kan sanggup alias buntu dilarang masuk narik cannot receive jobs alias tidak ada satu pun mereka yang bakal sukses nerima narik sisa gilir antrian tugas sisa deretan job sisanya tsb.