Hingga chapter ini, seluruh kode async kita berjalan sebagai satu task: satu deretan .await di atas block_on, sekuensial dari awal hingga akhir. tokio::spawn memecah batasan itu — fungsi ini menyerahkan sebuah future ke scheduler dan langsung kembali tanpa menunggu, sehingga runtime menjalankannya sebagai task mandiri di thread worker. Dari titik inilah janji Chapter Mengapa Async terpenuhi: ratusan ribu task penantinan I/O berbagi sedikit thread. Chapter ini mengukur konkuransi hasil spawn, membuktikan task benar-benar pindah ke thread worker, lalu menghadapi dua syarat ketat yang dituntut runtime dari task yang diluncurkan.

Meluncurkan Tugas dengan tokio::spawn #

Signature-nya ringkas: tokio::spawn(future) mengembalikan JoinHandle<T> seketika — tanpa satu detik penantinan pun. Task tersebut kemudian dijadwalkan runtime kapan pun worker tersedia, sementara kode pemanggil melanjutkan hidupnya. Pola lengkapnya: buat handle untuk setiap task, kerjakan hal lain bila perlu, lalu .await handle saat hasilnya benar-benar dibutuhkan:

rust
use std::time::{Duration, Instant};

#[tokio::main]
async fn main() {
    let start = Instant::now();

    let task_a = tokio::spawn(async {
        tokio::time::sleep(Duration::from_secs(1)).await;
        "laporan penjualan"
    });

    let task_b = tokio::spawn(async {
        tokio::time::sleep(Duration::from_secs(2)).await;
        "laporan stok"
    });

    let task_c = tokio::spawn(async {
        tokio::time::sleep(Duration::from_secs(3)).await;
        "laporan pengiriman"
    });

    let results = (
        task_a.await.unwrap(),
        task_b.await.unwrap(),
        task_c.await.unwrap(),
    );
    println!("hasil: {:?}", results);
    println!("total: {} detik", start.elapsed().as_secs());
}

Output yang Dihasilkan:

text
hasil: ("laporan penjualan", "laporan stok", "laporan pengiriman")
total: 3 detik

Mari kita bedah kode di atas:

  1. Baris 7-10 (let task_a = tokio::spawn(async { ... });): blok async menjadi future, spawn menyerahkannya ke scheduler, dan task_a mengikat JoinHandle<&str>. Pemanggilan ini kembali hampir seketika — ketika baris 7 selesai, belum ada satu detik pun berlalu karena ketiga task baru dibangun, bukan ditunggu.
  2. Baris 8 (tokio::time::sleep(Duration::from_secs(1)).await;): di dalam task, .await menjeda task tersebut 1 detik. Ketiga task menjalani jeda 1, 2, dan 3 detik secara serentak karena runtime memajukan mereka bergantian di thread worker.
  3. Baris 22-26 (task_a.await.unwrap(), ...): .await pada JoinHandle menunggu task selesai dan mengambil nilainya. Kita menunggu handle secara berurutan, tetapi tugas-tugasnya sendiri sudah berjalan serentak sejak awal.
  4. Baris 28 (println!("total: {} detik", ...)): total waktu 3 detik — sama dengan jeda terpanjang (task_c), bukan jumlah 1+2+3 = 6 detik. Inilah aritmetika konkuransi: spawn menumpangkan penantinan-penantinan yang tadinya akan berbaris seri di satu task.
  5. .unwrap() pada setiap handle: JoinHandle::await tidak mengembalikan &str langsung, melainkan Result<&str, JoinError>. Task bisa gagal di tengah jalan (misalnya panic!), dan kegagalan itu dilaporkan lewat JoinError ketika handle di-await — kita bedah penanganannya tuntas di Chapter Error di Async.

Alur kerja spawn dapat diringkas dalam diagram berikut:

Diagram di atas memperlihatkan arah tanggung jawab: spawn menyerahkan future kepada scheduler dan menerima JoinHandle sebagai tanda klaim, task hidup sendiri di thread worker, dan .await pada handle menjadi jembatan pengambilan hasil — persis hubungan spawn/join pada thread OS di Section Koncurrency, kini dengan unit yang jauh lebih ringan.

Handle juga boleh diabaikan sama sekali: pemanggilan tokio::spawn(...) tanpa mengikat hasilnya tetap sah — task tetap berjalan sampai tuntas, pola yang dikenal sebagai fire and forget. Yang hilang hanyalah jalan pulang: tanpa handle, kita tidak bisa menunggu hasil atau mendeteksi kegagalan task tersebut, dan bila runtime dibubarkan lebih dulu, task yang belum selesai berhenti di tengah jalan.

Tugas Berpindah ke Thread Worker #

Chapter Runtime tokio menyisakan satu janji yang belum dibuktikan: task hasil spawn berjalan di thread worker bernama tokio-rt-worker, bukan di thread main. Program ini menanyakannya langsung kepada sistem:

rust
#[tokio::main]
async fn main() {
    println!("main berjalan di thread {:?}", std::thread::current().name());

    let task1 = tokio::spawn(async {
        std::thread::current().name().unwrap().to_string()
    });
    let task2 = tokio::spawn(async {
        std::thread::current().name().unwrap().to_string()
    });

    let (name1, name2) = tokio::join!(task1, task2);
    println!("tugas 1 berjalan di thread {}", name1.unwrap());
    println!("tugas 2 berjalan di thread {}", name2.unwrap());
}

Output yang Dihasilkan:

text
main berjalan di thread Some("main")
tugas 1 berjalan di thread tokio-rt-worker
tugas 2 berjalan di thread tokio-rt-worker

Mari kita bedah kode di atas:

  1. Baris 3 (println!("main berjalan di thread {:?}", ...)): future teratas dari block_on (yang ditulis makro #[tokio::main]) dijalankan di thread pemanggil — nama thread-nya main, sesuai teori Chapter Runtime tokio.
  2. Baris 5-7 (let task1 = tokio::spawn(async { ... });): setiap task mengembalikan nama thread tempat task itu berjalan sebagai String. Task tidak mencetak sendiri hasilnya; dengan join! kita mengumpulkan hasilnya dulu agar urutan cetak di main tetap terkendali meski urutan penjadwalan task bisa bergeser-geser.
  3. Baris 12 (let (name1, name2) = tokio::join!(task1, task2);): menunggu kedua handle sekaligus; pola join! dari Chapter .await diterapkan pada JoinHandle seperti pada future biasa.
  4. Output tokio-rt-worker: kedua task tercatat berjalan di thread worker, terpisah dari thread main. Pada mesin banyak core, kedua task itu bisa saja terbagi ke worker berbeda — nama thread sama-sama tokio-rt-worker karena tokio tidak menomori workernya, dan task juga boleh berpindah worker di antara titik .await akibat work stealing.

Syarat Tugas: Send dan ‘static #

Kebebasan task meluncur dan berpindah thread menuntut dua jaminan yang dienforcer kompilator. Pertama, 'static: task boleh menghidupi lebih lama daripada fungsi yang meluncurkannya, sehingga future yang dikirim tidak boleh meminjam data lokal milik fungsi tersebut — pinjaman itu bisa basi ketika fungsinya sudah bubar. Kedua, Send: task dapat dipindahkan antar thread oleh scheduler, sehingga seluruh data di dalamnya harus aman dikirim lintas thread; kedua istilah ini sudah kita kenal dari trait Send/Sync di Section Koncurrency. Kode berikut melanggar syarat pertama:

rust
#[tokio::main]
async fn main() {
    let warehouse_name = String::from("gudang pusat");

    tokio::spawn(async {
        println!("memproses data dari {}", warehouse_name);
    });
}

Pesan galat kompilator:

text
error[E0373]: async block may outlive the current function, but it borrows `warehouse_name`, which is owned by the current function
 --> src/main.rs:5:18
  |
5 |     tokio::spawn(async {
  |                  ^^^^^ may outlive borrowed value `warehouse_name`
6 |         println!("memproses data dari {}", warehouse_name);
  |                                            -------------- `warehouse_name` is borrowed here
  |
  = note: async blocks are not executed immediately and must either take a reference or ownership of outside variables they use
help: to force the async block to take ownership of `warehouse_name` (and any other referenced variables), use the `move` keyword
  |
5 |     tokio::spawn(async move {
  |                        ++++

For more information about this error, try `rustc --explain E0373`.
error: could not compile `belajar-async` (bin "belajar-async") due to 1 previous error

Galat E0373 menjelaskan skenarionya gamblang: blok async dipanggil “may outlive the current function” — runtime bisa menyimpan task tersebut setelah main selesai — sementara blok itu meminjam warehouse_name milik main. Kompilator bahkan menuliskan obatnya: tambahkan move sehingga blok menjadi async move { ... } dan kepemilikan String berpindah ke task, persis perilaku move closure dari Section Closure. Satu catatan kecil: async fn main yang tidak mengembalikan nilai membuat program berakhir begitu tubuh main selesai — task yang masih berjalan bisa terhenti sebelum waktunya, karena program keluar tanpa menunggu task; menunggu handle atau runtime shutdown adalah cara memastikan task tuntas.

Ringkasan #

tokio::spawn menyerahkan future ke scheduler dan memulangkan JoinHandle seketika: tiga task berjeda 1, 2, dan 3 detik tuntas dalam 3 detik total — durasi terpanjang, bukan jumlah — karena penantinan tumpang tindih di thread worker. Bukti nama thread menegaskan pembagian kerja: future teratas di main, task hasil spawn di tokio-rt-worker, dengan work stealing yang boleh memindahkan task antar worker di setiap titik .await. Kebebasan itu dibayar dua syarat: future harus Send dan 'static, dijaga E0373 yang menolak pinjaman data lokal dan menyarankan async move. Handle yang di-await menghasilkan Result<T, JoinError> — sinyal bahwa task bisa gagal, dan topik itulah yang menutup section ini setelah I/O async dan select!.