HarmonyOS ArkTS Concurrency: Actors, TaskPools, and Worker Threads
Actor Model Overview
ArkTS utilizes the Acter model for concurrent execution. In this architecture, computation is divided into independent actors. Each actor encapsulates its own state and behavior, communicating exclusively through asynchronous message passing. This isolation eliminates shared memory states between actors, effectively preventing common concurrency issues such as race conditions and deadlocks.
When an actor receives a message, it processes the information based on its current internal state, performs necessary calculations, potentially updates its state, and dispatches messages to other actors or itself. Since communication is asynchronous, the sender does not wait for a response, allowing true parallelism.
Data Transfer Objects
Understanding how data moves between threads is critical for safe concurrency in ArkTS. There are four primary categories of transmittable objects:
1. Normal Objects
These cannot be directly transmitted or copied across threads; they exist only within the thread that created them. Serialization relies on the structured clone algorithm.
- Supported Types: Primitives (except Symbol), Date, String, RegExp, Array, Map, Set, simple Objects (created via
{}ornew Object()), ArrayBuffer, and TypedArray. - Limitation: Prototypes and methods are not transferred; only property values are serialized.
2. Transferable Objects
Designed for efficient bulk transfer without copying. Ownership moves from the sender to the receiver.
- Example:
ArrayBuffer. - Mechanism: Upon transfer, the sending thread loses access to the buffer. The receiving thread gains exclusive ownership.
- Benefit: High performance for large data blocks by avoiding serialization overhead.
// Defining a transferable buffer
const sendBuffer = new ArrayBuffer(256);
3. Shared Objects
Allows multiple threads to access the same memory region simultaneously.
- Example:
SharedArrayBuffer. - Synchronization: Must use
AtomicsAPI (e.g.,add,sub,and) for atomic operations to prevent data corruption. - Caution: Strictly requires explicit synchronization logic to avoid race conditions.
// Creating a shared buffer for multi-thread access
const sharedMem = new SharedArrayBuffer(2048);
4. Native Binding Objects
Provide direct access to underlying system capabilities like file systems or networking.
- Key Instances:
Context: Provides application component context.RemoteObject: Enables remote procedure calls and object reference sharing across processes.
TaskPool vs. Worker Threads
Choosing the right abstraction depends on the task requirements.
Feature Comparison
| Feature | TaskPool | Worker |
|---|---|---|
| Memory | Isolated per thread | Isolated per thread |
| Data Passing | Structured Clone + Transfer/Shared support | Structured Clone + Transfer/Shared support |
| Invocation | Pass function directly | Send messages to entry point |
| Return Value | Async Promise | Via postMessage handlers |
| Lifecycle | System managed (auto-scales) | Developer managed |
| Max Threads | Limited internally (approx. 4 active) | Max 8 per process |
| Cancellation | Supports queue cancellation | No built-in cancellation |
Execution Mechanisms
TaskPool Ideal for short-lived, independent tasks. You submit a function and parameters to the pool. The system assigns a worker thread automatically. Results return asynchronously. It abstracts away thread creation and destruction overhead.
Worker
Best for long-running services that need to persist across multiple requests. A Worker instance remains alive until explicitly terminated via terminate() or close(). Communication is strictly message-based (onmessage and postMessage).
Implementation Guidelines
CPU-Bound Tasks
Use TaskPool for compute-heavy operations like image processing where splitting workloads reduces UI blockage.
import taskpool from '@ohos.taskpool';
@Concurrent
function splitProcess(chunk: ArrayBuffer) {
// Simulate heavy calculation
return chunk;
}
async function handleImageWork(imageData: ArrayBuffer) {
const chunkSize = imageData.byteLength / 4;
const chunks = [
imageData.slice(0, chunkSize),
imageData.slice(chunkSize, chunkSize * 2),
imageData.slice(chunkSize * 2)
];
const promises = chunks.map(chunk => {
const t = new taskpool.Task(splitProcess, chunk);
return taskpool.execute(t);
});
await Promise.all(promises);
}
I/O-Bound Tasks
For disk operations, offload synchronous blocking calls to background threads using TaskPool.
import taskpool from '@ohos.taskpool';
import fs from '@ohos.file.fs';
@Concurrent
async function writeToFile(path: string, content: string) {
const file = await fs.open(path, fs.OpenMode.READ_WRITE | fs.OpenMode.CREATE);
try {
await fs.write(file.fd, content);
} finally {
fs.close(file);
}
return path;
}
async function batchWrite(paths: string[]) {
const tasks = paths.map(p => new taskpool.Task(writeToFile, p, 'Content'));
const results = await taskpool.batchExecute(tasks);
console.info(results);
}
Synchronous Coordination
Workers can perform synchronous actions, but must ensure thread safety when accessing static resources.
// Helper.ts
export default class DataManager {
private static instance: DataManager;
private storage: Record<string, any> = {};
public static getInstance(): DataManager {
if (!DataManager.instance) { DataManger.instance = new DataManager(); }
return DataManger.instance;
}
public set(key: string, value: any): void {
this.storage[key] = value;
}
}
// MainThread.ets
import taskpool from '@ohos.taskpool';
import DataManager from './Helper';
@Concurrent
function updateStore(id: number) {
DataManager.getInstance().set(`id_${id}`, 'processed');
return true;
}
async function runSyncTask() {
const task = new taskpool.Task(updateStore, 101);
await taskpool.execute(task);
}
Worker Lifecycle Management
Workers should be created once and reused if possible. Excessive creation degrades performance.
// Main Thread
import workerModule from '@ohos.worker';
const myWorker = new workerModule.ThreadWorker('entry/ets/workers/background.ts');
myWorker.onmessage = (e) => {
console.log('Got result:', e.data);
};
myWorker.postMessage({ action: 'start' });
// Cleanup
myWorker.terminate();
// Inside Worker.ts
import workerModule from '@ohos.worker';
const scope = workerModule.workerPort;
scope.onmessage = (e) => {
if (e.data.action === 'start') {
// Do work
scope.postMessage({ status: 'ready' });
}
};
// To exit manually
scope.close();
Note: TaskPool functions marked with @Concurrent require the code to reside in .ets files. Arguments passed to these functions must be serializable according to the rules defined in the Data Transfer section.