Menjalankan komputasi intensif langsung di main thread Node.js akan memblokir libuv event loop. Akibatnya, I/O polling, timer, dan resolusi Promise mengalami starvation. Solusi standar adalah mendelegasikan tugas ke native thread (C/C++ atau Rust) melalui Node-API (sebelumnya N-API). Namun, memanggil fungsi JavaScript secara langsung dari thread native di luar konteks V8 runtime dapat menyebabkan crash (segmentation fault) karena V8 thread-model bersifat single-threaded.
Sinkronisasi antara native thread dan event loop Node.js membutuhkan jembatan yang thread-safe. N-API menyediakan primitif napi_threadsafe_function (TSFN) untuk mengirimkan data dan mengeksekusi callback JavaScript kembali di main thread secara non-blocking.
Akar Masalah Starvation pada libuv Event Loop
Event loop Node.js berjalan di atas thread tunggal yang mengeksekusi siklus timers, pending callbacks, idle/prepare, poll, check, dan close callbacks. Saat sebuah modul native C++ dipanggil melalui add-on sinkron, V8 thread terkunci pada fungsi native tersebut hingga komputasi selesai.
Jika modul native membuat thread background OS standar (seperti std::thread atau pthread), thread tersebut tidak boleh langsung memanipulasi handle napi_value atau memanggil napi_call_function. Ruang memori V8 dan GC (Garbage Collector) hanya aman diakses dari thread tempat instance runtime berjalan. Pengiriman data hasil kerja thread native ke JavaScript harus masuk kembali ke antrean libuv loop.
Arsitektur napi_threadsafe_function
napi_threadsafe_function membungkus referensi fungsi JavaScript dan menyediakan antrean internal (thread-safe queue). Native worker thread dapat memanggil napi_call_threadsafe_function dari thread manapun. N-API kemudian memasukkan pesan tersebut ke antrean pesan loop libuv dan memicu callback di thread utama saat siklus event loop mencapai tahap pemrosesan callback.
// Alur komunikasi:
Native Worker Thread ---[napi_call_threadsafe_function]---> Queue Internal N-API
|
libuv Event Loop
|
(Main Thread / V8 Engine)
v
JS Callback ExecutionImplementasi Praktis C++ Native Worker
Berikut adalah implementasi worker native C++ menggunakan napi_threadsafe_function. Kode ini menerima callback dari JavaScript, menjalankan thread komputasi independen di latar belakang, dan mengembalikan data secara bertahap tanpa memblokir I/O.
#include <node_api.h>
#include <thread>
#include <chrono>
struct WorkContext {
napi_threadsafe_function tsfn;
};
// Callback yang dieksekusi di main thread oleh V8
void CallJsCallback(napi_env env, napi_value js_callback, void* context, void* data) {
if (env != nullptr && js_callback != nullptr) {
int* result_data = static_cast<int*>(data);
napi_value argv[1];
napi_create_int32(env, *result_data, &argv[0]);
napi_call_function(env, nullptr, js_callback, 1, argv, nullptr);
delete result_data;
}
}
// Background worker thread native
void NativeWorker(WorkContext* ctx) {
for (int i = 1; i <= 5; ++i) {
std::this_thread::sleep_for(std::chrono::milliseconds(200)); // Simulasi beban komputasi
int* payload = new int(i * 100);
// Kirim hasil ke main thread secara non-blocking
napi_status status = napi_call_threadsafe_function(
ctx->tsfn,
payload,
napi_tsfn_nonblocking
);
if (status != napi_ok) {
delete payload;
break;
}
}
// Deklarasikan selesai agar reference counter berkurang
napi_release_threadsafe_function(ctx->tsfn, napi_tsfn_release);
delete ctx;
}
// Entrypoint fungsi yang diekspos ke Node.js
napi_value StartWorker(napi_env env, napi_callback_info info) {
size_t argc = 1;
napi_value args[1];
napi_get_cb_info(env, info, &argc, args, nullptr, nullptr);
napi_value resource_name;
napi_create_string_utf8(env, "NativeWorkerResource", NAPI_AUTO_LENGTH, &resource_name);
WorkContext* ctx = new WorkContext();
// Buat Threadsafe Function
napi_create_threadsafe_function(
env,
args[0], // JS Callback
nullptr,
resource_name,
0, // Max queue size (0 = tak terbatas)
1, // Initial thread count
nullptr,
nullptr,
ctx,
CallJsCallback,
&(ctx->tsfn)
);
std::thread worker(NativeWorker, ctx);
worker.detach();
return nullptr;
}
napi_value Init(napi_env env, napi_value exports) {
napi_value fn;
napi_create_function(env, nullptr, 0, StartWorker, nullptr, &fn);
napi_set_named_property(env, exports, "startWorker", fn);
return exports;
}
NAPI_MODULE(NODE_GYP_MODULE_NAME, Init)Manajemen Backpressure dan Batas Antrean
Penggunaan antrean tanpa batas (max_queue_size = 0) dapat memicu lonjakan memori (out-of-memory crash) jika native thread memproduksi data lebih cepat dibanding kapasitas pemrosesan JavaScript engine di event loop.
Untuk mengontrol backpressure:
- Tentukan batas kapasitas buffer pada parameter
max_queue_sizedinapi_create_threadsafe_function. - Gunakan flag
napi_tsfn_blockingsaat memanggilnapi_call_threadsafe_functiondari native thread jika kapasitas antrean penuh. Native thread akan masuk ke kondisi sleep secara otomatis sampai main thread mengonsumsi pesan di antrean, mencegah akumulasi memori yang tidak terkontrol. - Jika latensi native thread kritis dan data lama dapat diabaikan, gunakan flag
napi_tsfn_nonblockingdan tangani kembaliannapi_queue_fulluntuk membuang frame data (drop frames).
Minimalisasi Mutex Locking dan Pencegahan Deadlock
Penggunaan std::mutex di sekitar N-API harus diminimalkan. Kesalahan fatal yang sering terjadi adalah mengunci mutex di native thread, lalu menunggu operasi JavaScript selesai melalui TSFN sementara main thread sedang mencoba mengakuisisi mutex yang sama. Skenario ini memicu deadlock permanen.
Terapkan prinsip arsitektur bebas kunci (lock-free) atau batasi cakupan lock:
- Data Duplication/Transfer Ownership: Alih-alih berbagi memori menggunakan mutex, transfer kepemilikan data (move semantics pada C++ atau alokasi heap baru) langsung ke callback payload.
- Never Block the Main Thread: Jangan pernah memanggil operasi sinkronisasi native yang memblokir (seperti
std::condition_variable::waitataupthread_join) di dalam callback konteks N-API/main thread yang menunggu respons worker thread.
Pencegahan Memory Leak dan Lifecycle Management
Pengelolaan siklus hidup objek native dan referensi JavaScript memerlukan kepatuhan terhadap aturan kepemilikan N-API:
- Reference Counting TSFN: Setiap
napi_create_threadsafe_functionmengunci reference loop Node.js agar proses tidak exit prematur. Panggilnapi_acquire_threadsafe_functionsaat menambah thread produsen, dan selalu pasangkan dengannapi_release_threadsafe_functionsaat thread selesai bekerja. Kegagalan memanggil release akan menyebabkan proses Node.js menggantung (hang) saat program dihentikan. - Pembersihan Payload: Data heap yang dialokasikan di thread native (seperti pointer
payloadpada contoh di atas) harus didealokasikan (delete/free) di dalam fungsi callback penjamin (CallJsCallback) setelah konversi kenapi_valueberhasil.
Komentar
0 komentar
Masuk ke akun kamu untuk ikut berkomentar.
Belum ada komentar
Jadilah yang pertama ikut berdiskusi!