Async Operations with libuv
Node.js is single-threaded with an event loop. Long-running C++ operations must run asynchronously to avoid blocking. This chapter covers libuv integration, worker threads, and async patterns.
The Event Loop Problem
Why Async is Critical
Synchronous operation (BLOCKS event loop):
bad-sync.cc:
void ProcessImage(const FunctionCallbackInfo<Value>& args) {
Isolate* isolate = args.GetIsolate();
// This takes 5 seconds and BLOCKS everything!
uint8_t* result = ExpensiveImageProcessing(inputData, inputSize);
// Event loop frozen for 5 seconds
// - No HTTP requests handled
// - No timers fire
// - No I/O operations complete
// - Application appears frozen
args.GetReturnValue().Set(/* result */);
}
Impact:
Timeline:
0ms: processImage() called
0-5000ms: EVENT LOOP BLOCKED
- Server can't respond to requests
- Scheduled callbacks don't run
- Application unusable
5000ms: Function returns
5000ms+: Event loop resumes
The Rule: Operations > 10ms must be async!
libuv Work Queue
Understanding libuv
libuv is Node.js's cross-platform async I/O library:
- Manages thread pool for async operations
- Handles file system operations
- Manages network I/O
- Provides platform abstraction (Windows, Linux, macOS)
Default thread pool size: 4 threads
Configure thread pool:
UV_THREADPOOL_SIZE=8 node app.js
Basic Async Pattern
async-work.cc:
#include <node.h>
#include <uv.h>
#include <iostream>
namespace demo {
using v8::Context;
using v8::Function;
using v8::FunctionCallbackInfo;
using v8::Isolate;
using v8::Local;
using v8::Number;
using v8::Object;
using v8::Persistent;
using v8::String;
using v8::Value;
// Structure to pass data between threads
struct WorkData {
uv_work_t request; // libuv work request
Persistent<Function> callback; // JavaScript callback
Isolate* isolate;
// Input data
std::string input;
// Output data
std::string result;
std::string error;
};
// Executed in worker thread (NOT main thread!)
void ExecuteWork(uv_work_t* req) {
WorkData* data = static_cast<WorkData*>(req->data);
// This runs in background thread
// DO NOT access V8/JavaScript objects here!
try {
// Simulate expensive operation
std::this_thread::sleep_for(std::chrono::seconds(2));
// Actual work (CPU-intensive operation)
data->result = "Processed: " + data->input;
} catch (const std::exception& e) {
data->error = e.what();
}
}
// Executed on main thread after work completes
void WorkComplete(uv_work_t* req, int status) {
WorkData* data = static_cast<WorkData*>(req->data);
Isolate* isolate = data->isolate;
v8::HandleScope handleScope(isolate);
Local<Context> context = isolate->GetCurrentContext();
// Prepare callback arguments
const unsigned argc = 2;
Local<Value> argv[argc];
if (!data->error.empty() || status != 0) {
// Error occurred
argv[0] = String::NewFromUtf8(
isolate,
data->error.c_str()
).ToLocalChecked();
argv[1] = v8::Null(isolate);
} else {
// Success
argv[0] = v8::Null(isolate);
argv[1] = String::NewFromUtf8(
isolate,
data->result.c_str()
).ToLocalChecked();
}
// Call JavaScript callback
Local<Function> callback = Local<Function>::New(isolate, data->callback);
callback->Call(context, v8::Null(isolate), argc, argv).ToLocalChecked();
// Cleanup
data->callback.Reset();
delete data;
}
// Function called from JavaScript
void ProcessAsync(const FunctionCallbackInfo<Value>& args) {
Isolate* isolate = args.GetIsolate();
Local<Context> context = isolate->GetCurrentContext();
// Validate arguments
if (args.Length() < 2 || !args[0]->IsString() || !args[1]->IsFunction()) {
isolate->ThrowException(v8::Exception::TypeError(
String::NewFromUtf8(isolate,
"Expected (string, callback)").ToLocalChecked()));
return;
}
// Prepare work data
WorkData* data = new WorkData();
data->request.data = data;
data->isolate = isolate;
// Get input
String::Utf8Value input(isolate, args[0]);
data->input = std::string(*input);
// Store callback
Local<Function> callback = Local<Function>::Cast(args[1]);
data->callback.Reset(isolate, callback);
// Queue work
uv_queue_work(
uv_default_loop(),
&data->request,
ExecuteWork, // Runs in worker thread
WorkComplete // Runs on main thread when done
);
// Return immediately (non-blocking)
}
void Initialize(Local<Object> exports, Local<Object> module) {
NODE_SET_METHOD(exports, "processAsync", ProcessAsync);
}
NODE_MODULE(NODE_GYP_MODULE_NAME, Initialize)
} // namespace demo
JavaScript usage:
const addon = require('./build/Release/addon');
console.log('Starting async operation...');
addon.processAsync('test data', (err, result) => {
if (err) {
console.error('Error:', err);
return;
}
console.log('Result:', result);
// Output: Result: Processed: test data
});
console.log('Operation queued, continuing...');
// This runs immediately!
// Timeline:
// 0ms: processAsync called
// 0ms: Work queued
// 0ms: JavaScript continues (non-blocking!)
// 0-2000ms: Worker thread processing
// 2000ms: Callback executed with result
Key Concepts:
1. WorkData structure:
struct WorkData {
uv_work_t request; // Required by libuv
Persistent<Function> callback; // Must persist across threads
Isolate* isolate; // For callback execution
// Your data
std::string input;
std::string result;
};
2. Thread separation:
// Worker thread (background)
void ExecuteWork(uv_work_t* req) {
// DO NOT access V8 objects!
// Only manipulate C++ data
}
// Main thread (after completion)
void WorkComplete(uv_work_t* req, int status) {
// CAN access V8 objects
// Call JavaScript callback
}
3. Memory management:
WorkData* data = new WorkData(); // Allocate
// ...
delete data; // Free in WorkComplete
Promise-Based Async
Modern JavaScript uses Promises. Here's how to return Promises from C++:
async-promise.cc:
#include <node.h>
#include <uv.h>
namespace demo {
using v8::Context;
using v8::FunctionCallbackInfo;
using v8::Isolate;
using v8::Local;
using v8::Object;
using v8::Persistent;
using v8::Promise;
using v8::String;
using v8::Value;
struct PromiseWorkData {
uv_work_t request;
Persistent<Promise::Resolver> resolver;
Isolate* isolate;
std::string input;
std::string result;
std::string error;
};
void ExecutePromiseWork(uv_work_t* req) {
PromiseWorkData* data = static_cast<PromiseWorkData*>(req->data);
try {
// Expensive operation
std::this_thread::sleep_for(std::chrono::seconds(1));
data->result = "Processed: " + data->input;
} catch (const std::exception& e) {
data->error = e.what();
}
}
void PromiseWorkComplete(uv_work_t* req, int status) {
PromiseWorkData* data = static_cast<PromiseWorkData*>(req->data);
Isolate* isolate = data->isolate;
v8::HandleScope handleScope(isolate);
Local<Context> context = isolate->GetCurrentContext();
Local<Promise::Resolver> resolver =
Local<Promise::Resolver>::New(isolate, data->resolver);
if (!data->error.empty() || status != 0) {
// Reject promise
Local<Value> error = String::NewFromUtf8(
isolate,
data->error.c_str()
).ToLocalChecked();
resolver->Reject(context, error).ToChecked();
} else {
// Resolve promise
Local<Value> result = String::NewFromUtf8(
isolate,
data->result.c_str()
).ToLocalChecked();
resolver->Resolve(context, result).ToChecked();
}
// Cleanup
data->resolver.Reset();
delete data;
}
void ProcessPromise(const FunctionCallbackInfo<Value>& args) {
Isolate* isolate = args.GetIsolate();
Local<Context> context = isolate->GetCurrentContext();
// Create promise
Local<Promise::Resolver> resolver = Promise::Resolver::New(context)
.ToLocalChecked();
Local<Promise> promise = resolver->GetPromise();
// Prepare work
PromiseWorkData* data = new PromiseWorkData();
data->request.data = data;
data->isolate = isolate;
data->resolver.Reset(isolate, resolver);
String::Utf8Value input(isolate, args[0]);
data->input = std::string(*input);
// Queue work
uv_queue_work(
uv_default_loop(),
&data->request,
ExecutePromiseWork,
PromiseWorkComplete
);
// Return promise immediately
args.GetReturnValue().Set(promise);
}
void Initialize(Local<Object> exports) {
NODE_SET_METHOD(exports, "processPromise", ProcessPromise);
}
NODE_MODULE(NODE_GYP_MODULE_NAME, Initialize)
} // namespace demo
JavaScript usage (modern):
const addon = require('./build/Release/addon');
// Using Promises
addon.processPromise('hello')
.then(result => {
console.log('Result:', result);
})
.catch(err => {
console.error('Error:', err);
});
// Using async/await
async function process() {
try {
const result = await addon.processPromise('world');
console.log('Result:', result);
} catch (err) {
console.error('Error:', err);
}
}
process();
Progress Reporting
For long-running operations, report progress back to JavaScript.
async-progress.cc:
#include <node.h>
#include <uv.h>
namespace demo {
using v8::Context;
using v8::Function;
using v8::FunctionCallbackInfo;
using v8::Isolate;
using v8::Local;
using v8::Number;
using v8::Object;
using v8::Persistent;
using v8::String;
using v8::Value;
struct ProgressData {
uv_work_t request;
uv_async_t async;
Persistent<Function> progressCallback;
Persistent<Function> doneCallback;
Isolate* isolate;
int totalWork;
int currentProgress;
std::string result;
bool completed;
};
// Called on main thread when async_send is called
void ProgressCallback(uv_async_t* handle) {
ProgressData* data = static_cast<ProgressData*>(handle->data);
Isolate* isolate = data->isolate;
v8::HandleScope handleScope(isolate);
Local<Context> context = isolate->GetCurrentContext();
if (data->completed) {
// Work is done
Local<Function> callback = Local<Function>::New(
isolate, data->doneCallback);
Local<Value> argv[1] = {
String::NewFromUtf8(isolate, data->result.c_str()).ToLocalChecked()
};
callback->Call(context, v8::Null(isolate), 1, argv).ToLocalChecked();
// Cleanup
uv_close((uv_handle_t*)&data->async, nullptr);
data->progressCallback.Reset();
data->doneCallback.Reset();
delete data;
} else {
// Report progress
Local<Function> callback = Local<Function>::New(
isolate, data->progressCallback);
Local<Value> argv[1] = {
Number::New(isolate, data->currentProgress)
};
callback->Call(context, v8::Null(isolate), 1, argv).ToLocalChecked();
}
}
void ExecuteWithProgress(uv_work_t* req) {
ProgressData* data = static_cast<ProgressData*>(req->data);
for (int i = 0; i < data->totalWork; i++) {
// Do work
std::this_thread::sleep_for(std::chrono::milliseconds(100));
// Update progress
data->currentProgress = ((i + 1) * 100) / data->totalWork;
// Notify main thread
uv_async_send(&data->async);
}
data->result = "Work completed!";
data->completed = true;
}
void ProgressWorkComplete(uv_work_t* req, int status) {
ProgressData* data = static_cast<ProgressData*>(req->data);
// Send final notification
uv_async_send(&data->async);
}
void ProcessWithProgress(const FunctionCallbackInfo<Value>& args) {
Isolate* isolate = args.GetIsolate();
// Validate arguments
if (args.Length() < 3 || !args[0]->IsNumber() ||
!args[1]->IsFunction() || !args[2]->IsFunction()) {
isolate->ThrowException(v8::Exception::TypeError(
String::NewFromUtf8(isolate,
"Expected (number, progressCallback, doneCallback)")
.ToLocalChecked()));
return;
}
// Prepare data
ProgressData* data = new ProgressData();
data->request.data = data;
data->async.data = data;
data->isolate = isolate;
data->totalWork = args[0].As<Number>()->Value();
data->currentProgress = 0;
data->completed = false;
// Store callbacks
Local<Function> progressCb = Local<Function>::Cast(args[1]);
Local<Function> doneCb = Local<Function>::Cast(args[2]);
data->progressCallback.Reset(isolate, progressCb);
data->doneCallback.Reset(isolate, doneCb);
// Initialize async handle
uv_async_init(uv_default_loop(), &data->async, ProgressCallback);
// Queue work
uv_queue_work(
uv_default_loop(),
&data->request,
ExecuteWithProgress,
ProgressWorkComplete
);
}
void Initialize(Local<Object> exports) {
NODE_SET_METHOD(exports, "processWithProgress", ProcessWithProgress);
}
NODE_MODULE(NODE_GYP_MODULE_NAME, Initialize)
} // namespace demo
JavaScript usage:
const addon = require('./build/Release/addon');
addon.processWithProgress(
100, // Total work units
(progress) => {
console.log(`Progress: ${progress}%`);
},
(result) => {
console.log('Complete:', result);
}
);
// Output:
// Progress: 10%
// Progress: 20%
// ...
// Progress: 100%
// Complete: Work completed!
How it works:
1. uv_async_t handle:
uv_async_t async;
uv_async_init(uv_default_loop(), &async, ProgressCallback);
Creates a handle to send notifications from worker to main thread.
2. Send notification:
uv_async_send(&data->async); // From worker thread
Triggers ProgressCallback on main thread (safe!).
3. Cleanup:
uv_close((uv_handle_t*)&data->async, nullptr);
Close async handle when done.
Cancellation
Allow cancelling long-running operations:
cancellable.cc:
#include <node.h>
#include <uv.h>
#include <atomic>
namespace demo {
struct CancellableData {
uv_work_t request;
Persistent<Function> callback;
Isolate* isolate;
std::atomic<bool> cancelled;
std::string result;
};
void ExecuteCancellable(uv_work_t* req) {
CancellableData* data = static_cast<CancellableData*>(req->data);
for (int i = 0; i < 100; i++) {
// Check if cancelled
if (data->cancelled.load()) {
data->result = "Cancelled";
return;
}
// Do work
std::this_thread::sleep_for(std::chrono::milliseconds(100));
}
data->result = "Completed";
}
void CancellableComplete(uv_work_t* req, int status) {
CancellableData* data = static_cast<CancellableData*>(req->data);
Isolate* isolate = data->isolate;
v8::HandleScope handleScope(isolate);
Local<Context> context = isolate->GetCurrentContext();
Local<Function> callback = Local<Function>::New(isolate, data->callback);
Local<Value> argv[1] = {
String::NewFromUtf8(isolate, data->result.c_str()).ToLocalChecked()
};
callback->Call(context, v8::Null(isolate), 1, argv).ToLocalChecked();
data->callback.Reset();
delete data;
}
// Global pointer to current work (simplified for demo)
CancellableData* currentWork = nullptr;
void StartCancellable(const FunctionCallbackInfo<Value>& args) {
Isolate* isolate = args.GetIsolate();
CancellableData* data = new CancellableData();
data->request.data = data;
data->isolate = isolate;
data->cancelled.store(false);
Local<Function> callback = Local<Function>::Cast(args[0]);
data->callback.Reset(isolate, callback);
currentWork = data;
uv_queue_work(
uv_default_loop(),
&data->request,
ExecuteCancellable,
CancellableComplete
);
}
void CancelWork(const FunctionCallbackInfo<Value>& args) {
if (currentWork) {
currentWork->cancelled.store(true);
currentWork = nullptr;
}
}
void Initialize(Local<Object> exports) {
NODE_SET_METHOD(exports, "startCancellable", StartCancellable);
NODE_SET_METHOD(exports, "cancelWork", CancelWork);
}
NODE_MODULE(NODE_GYP_MODULE_NAME, Initialize)
} // namespace demo
JavaScript usage:
addon.startCancellable((result) => {
console.log('Result:', result);
});
// Cancel after 2 seconds
setTimeout(() => {
addon.cancelWork();
console.log('Cancellation requested');
}, 2000);
// Output: Result: Cancelled
Important: Cancellation is cooperative. Worker must check cancelled flag periodically.
Summary
Async operations are essential for high-performance addons:
Key Patterns:
- uv_queue_work: Run work in thread pool
- uv_async_t: Send notifications from worker to main thread
- Promises: Modern async pattern for JavaScript
- Progress reporting: Keep user informed during long operations
- Cancellation: Allow users to stop operations
Critical Rules:
- ✓ Never block event loop (> 10ms = must be async)
- ✓ Don't touch V8 objects in worker threads
- ✓ Use
Persistent<>for callbacks across threads - ✓ Always cleanup in completion handler
- ✓ Check for errors and handle them
Thread Safety:
// SAFE in worker thread
std::string result = ProcessData(cppData);
// UNSAFE in worker thread (CRASH!)
Local<String> str = String::NewFromUtf8(isolate, "bad");
// SAFE in main thread (WorkComplete)
Local<String> str = String::NewFromUtf8(isolate, "good");
Next: Object wrapping for C++ classes