Runtime asynchronous Rust seperti Tokio mengandalkan model cooperative scheduling. Task yang berjalan di dalam worker thread diasumsikan melepaskan kontrol secara sukarela melalui .await. Ketika komputasi CPU-heavy dieksekusi langsung di dalam async task tanpa titik yield, worker thread runtime akan terblokir. Kondisi ini memicu runtime starvation: task I/O lain pada thread yang sama tertunda, queue epoll/kqueue lambat dikosongkan, dan latency P99 melonjak drastis.

Akar Masalah: Concurrency vs Parallelism di Rust

Concurrency berkaitan dengan struktur aplikasi dalam menangani banyak hal sekaligus (misal: menangani ribuan koneksi TCP idle via non-blocking I/O). Parallelism berfokus pada eksekusi komputasi secara bersamaan di banyak core hardware fisik (misal: hashing kriptografi atau image processing).

Tokio didesain untuk concurrency I/O throughput tinggi dengan thread pool berukuran kecil (standarnya sama dengan jumlah core CPU logis). Menjalankan kalkulasi paralel atau CPU-bound langsung di thread pool Tokio mencampuradukkan abstraksi ini. Jika runtime memiliki 8 worker thread dan ada 8 task melakukan parsing JSON masif atau enkripsi secara sinkron, seluruh event loop lumpuh total.

Anti-Pattern: Memanggil CPU-Intensive Code di Tokio Worker

Contoh berikut mendemonstrasikan kesalahan umum: mengeksekusi fungsi komputasi berat di dalam async handler HTTP atau event loop utama.

use axum::{routing::get, Router};
use std::net::SocketAddr;

fn cpu_heavy_hashing(payload: Vec<u8>) -> [u8; 32] {
    // Simulasi kalkulasi intensif yang memakan 100ms CPU time
    let mut hash = [0u8; 32];
    for _ in 0..5_000_000 {
        hash[0] = hash[0].wrapping_add(payload.len() as u8);
    }
    hash
}

// ANTI-PATTERN: Memblokir worker thread Tokio
async fn bad_handler() -> &'static str {
    let dummy_data = vec![1u8; 1024];
    let _ = cpu_heavy_hashing(dummy_data); // Worker terblokir di sini!
    "Done"
}

Dampak langsung: request I/O ringan (misalnya /healthz) yang masuk ke worker thread yang sama akan tertahan hingga kalkulasi selesai. Throughput total server anjlok dan tail latency melonjak.

Solusi 1: tokio::task::spawn_blocking

Untuk operasi sinkron yang bersifat blocking I/O (seperti membaca disk secara blocking) atau komputasi berdurasi pendek, Tokio menyediakan spawn_blocking. Task ini dieksekusi di thread pool khusus terpisah dari worker event loop.

use tokio::task;

async fn blocking_handler() -> Result<[u8; 32], task::JoinError> {
    let dummy_data = vec![1u8; 1024];
    
    // Dipindahkan ke dedicated blocking pool Tokio
    let res = task::spawn_blocking(move || {
        cpu_heavy_hashing(dummy_data)
    }).await?;

    Ok(res)
}

Batasan: Thread pool spawn_blocking Tokio secara default dapat bertumbuh hingga 512 thread (bergantung konfigurasi). Jika dibebani throughput komputasi tinggi, overhead context switching kernel OS akan meningkat, mengurangi efisiensi utilisasi CPU cache.

Solusi 2: Menjembatani Rayon via tokio::sync::oneshot

Rayon dirancang khusus untuk komputasi data-paralel dengan arsitektur work-stealing. Ukuran pool Rayon umumnya dipatok tetap sesuai jumlah core fisik. Untuk mengisolasi CPU task dari Tokio tanpa memicu thread-explosion, buat jembatan menggunakan tokio::sync::oneshot.

use tokio::sync::oneshot;

pub async fn run_on_rayon<F, R>(func: F) -> Result<R, oneshot::error::RecvError>
where
    F: FnOnce() -> R + Send + 'static,
    R: Send + 'static,
{
    let (tx, rx) = oneshot::channel();

    rayon::spawn(move || {
        // Eksekusi di pool Rayon terisolasi
        let result = func();
        // Kirim hasil kembali ke runtime async Tokio
        let _ = tx.send(result);
    });

    rx.await
}

// Handler aman
async fn rayon_handler() -> Result<String, String> {
    let dummy_data = vec![1u8; 1024];
    
    let res = run_on_rayon(move || {
        cpu_heavy_hashing(dummy_data)
    })
    .await
    .map_err(|_| "Task cancelled or Rayon pool panicked".to_string())?;

    Ok(format!("Hash: {:?}", res))
}

Panduan Pemilihan Strategi

  • Tetap di Tokio (In-task): Operasi murni non-blocking I/O, kalkulasi CPU trivial (< 10–50 mikrosekon).
  • tokio::task::spawn_blocking: Panggilan I/O sinkron (interaksi filesystem OS standar, legacy blocking C libraries).
  • Rayon + oneshot bridge: Operasi CPU terberat (kriptografi, machine learning inference, kompresi Zstd/Gzip, pemrosesan citra, sorting data masif via par_iter).

Dampak Terhadap Karakteristik Latency

Dalam skenario pengujian dengan traffic I/O campuran (90% light I/O request, 10% CPU-heavy payload):

  • Unisolated (Anti-pattern): Latency P99 pada light I/O terdegradasi sebanding dengan durasi CPU task (naik dari <5ms ke >120ms) akibat thread starvation.
  • Rayon Bridge Isolation: Latency P99 light I/O tetap stabil di rentang sub-10ms karena Tokio worker thread tidak pernah terhalang kalkulasi CPU.