Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

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).

Filename: src/main.rs
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();
}
Listing 21-10: Menyimulasikan sebuah request yang pelan dengan cara menidurkan server selama lima detik

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.

Filename: src/main.rs
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();
}
Listing 21-11: Menelurkan sebuah thread baru buat masing-masing stream

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.

Filename: src/main.rs
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();
}
Listing 21-12: Antarmuka (interface) ThreadPool ideal milik kita

Kita 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:

Filename: src/lib.rs
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:

Filename: 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:

Filename: src/lib.rs
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:

Filename: src/lib.rs
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.

Filename: src/lib.rs
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,
    {
    }
}
Listing 21-13: Mengimplementasikan ThreadPool::new supaya dia panic kalau size-nya nol

Kita 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.

Filename: src/lib.rs
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,
    {
    }
}
Listing 21-14: Membikin vector buat ThreadPool buat menampung para threads yang ada

Kita 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:

  1. Definisikan sebuah struct Worker yang menampung (holds) sebuah id dan sebuah JoinHandle<()>.
  2. Ubah ThreadPool supaya dia itu sekarang malah nampung sebuah vector berisi instances dari Worker.
  3. Definisikan sebuah fungsi Worker::new yang nerima sebuah angka (number) buat jadi id terus dia ngembaliin sebuah instance Worker yang nampung si id itu beserta sebuah thread baru yang ditetaskan (spawned) memakai sebuah closure yang kosong.
  4. Di dalam ThreadPool::new, pakailah angka penghitung (counter) yang asalnya dari loop for itu buat di-generate (dijadiin) sebuah id, bikin sebuah Worker baru pakai id tadi, terus masukin dan simpan si Worker baru 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.

Filename: src/lib.rs
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 }
    }
}
Listing 21-15: Memodifikasi ThreadPool supaya dia menampung instances Worker ketimbang menampung threads secara langsung

Kita 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::spawn itu 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 memakai std::thread::Builder barengan sama method spawn-nya karena method tersebut nge-return (ngembaliin) tipe Result sebagai 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):

  1. ThreadPool bakal membikin (create) sebuah channel (saluran) dan berpegang erat (hold on to) pada bagian ujung pengirimnya (sender).
  2. Masing-masing Worker bakal berpegangan (hold on to) pada bagian penerimanya (receiver).
  3. Kita bakal membikin sebuah struct Job baru yang mana tugasnya buat nampung (hold) closures yang pengen kita kirimkan masuk menyusuri (down) si channel tersebut.
  4. Method execute bakal mengirim (send) si job (pekerjaan) yang mau dia eksekusi (execute) tersebut melalui si sender (pengirim) ini.
  5. Di dalam thread masing-masing, si Worker ini 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.

Filename: src/lib.rs
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 }
    }
}
Listing 21-16: Memodifikasi ThreadPool buat menyimpan sender dari sebuah channel yang mentransmisikan instances Job

Di 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.

Filename: src/lib.rs
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 }
    }
}
Listing 21-17: Mengoper receiver ke masing-masing Worker

Kita 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.

Filename: src/lib.rs
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 }
    }
}
Listing 21-18: Membagikan (sharing) isi receiver ini di antara para instances Worker tersebut dengan cara makek Arc dan Mutex

Di 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.

Filename: src/lib.rs
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 }
    }
}
Listing 21-19: Ngebikin type alias buat si Job supaya nampung sebuah Box yang aslinya berisi masing-masing closure dan terus ngirimin si job ini menelusuri ke dalem channelnya

Sehabis 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.

Filename: src/lib.rs
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 }
    }
}
Listing 21-20: Menerima dan juga sekaligus ngejalanin berbagai rupa jobs di dalam isi dari thread instance Worker tersebut

Di 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.

Filename: src/lib.rs
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 }
    }
}
Listing 21-21: Alternatif rancangan rupa implementasi buat kode eksekusi dari si Worker::new yang coba-coba ngandelin while let

Jujur 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<MutexGuard> yang emang udah jadi kodrat nasib jatah tugas buat metode dari fitur bernama lock 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 tersebut.

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.