Mastering HarmonyOS Concurrency: TaskPool, Worker & TaskGroup in AI Photo App
Through an AI ID photo tool case study, this article demonstrates HarmonyOS concurrency patterns: TaskPool for short preprocessing, Worker for long AI inference, TaskGroup for parallel batch generation, SequenceRunner for dependent tasks, SharedArrayBuffer for zero-copy sharing, Sendable for thread-safe singletons, and producer-consumer queues for real-time previews, with before/after performance metrics.
Introduction
This article explores HarmonyOS application concurrency design through a real-world AI ID photo tool. The author identifies common issues like UI freezing and frame drops caused by improper concurrency design, then systematically refactors the app using HarmonyOS concurrency primitives.
HarmonyOS Concurrency Architecture
The framework comprises five components:
Main Thread : Executes UI business, non-time-consuming operations, and single I/O tasks; shares system I/O thread pool with other ArkTS threads.
TaskPool : High-concurrency task pool for time-consuming tasks; encapsulates task entry, statistics module load; developers need not manage thread lifecycle.
Worker Thread : For resident tasks, CPU-intensive and long-running tasks; limited to 64 threads.
FFRT Task Pool : Scheduling pool for system tasks and user C/C++ time-consuming tasks.
Pthread : For C/C++ modules, background or time-consuming ArkTS-unrelated business; no thread limit.
Traditional Shared-Memory vs. ArkTS Concurrency Model
Traditional model : Uses threads and locks; threads share memory and protect critical sections with locks. For I/O or lock-containing business, multiple threads are spawned to avoid blocking, often resulting in hundreds of threads, increasing scheduling overhead and memory footprint.
ArkTS model : Employs memory-isolated thread model; threads communicate via messages, run lock-free internally. Business I/O operations are dispatched to background I/O task pools, not blocking ArkTS upper logic. Asynchronous I/O does not block ArkTS threads; TaskPool and I/O thread pools are managed uniformly by the system, greatly improving energy efficiency.
TaskPool vs Worker Comparison
Applicable Scenarios : TaskPool for short, independent tasks; Worker for long-running, resident tasks.
Lifecycle Management : TaskPool automatic by system; Worker manual by developer.
Thread Count : TaskPool core count - 1 (auto scaling); Worker max 64 (manual control).
Memory Overhead : TaskPool low; Worker ~2MB per thread.
Task Scheduling : TaskPool system-level with priority support; Worker developer-managed.
AI ID Photo Tool Concurrency Refactoring
Original Problem: All Operations on Main Thread
// ❌ Wrong example: all operations on main thread
async function processIDPhoto(imageUri: string): Promise<void> {
this.isProcessing = true;
// 1. Image preprocessing - pixel loop blocks main thread
const pixelMap = await loadAndPreprocessImage(imageUri);
// 2. AI inference - 2-3s CPU-intensive, UI completely unresponsive
const mask = await runMindSporeInference(pixelMap);
// 3. Post-processing to generate ID photo
const result = await generateIDPhoto(pixelMap, mask);
// 4. Update UI
this.showResult(result);
this.isProcessing = false;
}All time-consuming operations run on the main thread, blocking UI. Clicking "Start Processing" freezes the UI thread; if processing exceeds 5 seconds, the system shows "Application Not Responding".
Solution 1: TaskPool for Image Preprocessing
For independent, short-duration tasks like image preprocessing, TaskPool is optimal.
What is TaskPool?
TaskPool is HarmonyOS's high-concurrency task pool; developers wrap tasks and submit to TaskPool; system automatically manages thread creation, scheduling, and destruction.
Refactored Code
// imagePreprocess.ets
// 1. Mark as concurrent function with @Concurrent decorator
@Concurrent
export async function preprocessImage(imageUri: string): Promise<ArrayBuffer> {
console.info('🚀 [TaskPool] Starting image preprocessing');
// Open file
let file = fileIo.openSync(imageUri, fileIo.OpenMode.READ_ONLY);
let imageSource = image.createImageSource(file.fd);
let pixelMap = await imageSource.createPixelMapSync();
// Get original dimensions
const { width: originalWidth, height: originalHeight } = pixelMap.getImageInfoSync();
console.info(`📐 Original size: ${originalWidth}x${originalHeight}`);
// Scale to model input size (1024x1024)
pixelMap.scaleSync(1024 / originalWidth, 1024 / originalHeight);
// Read pixel data
let readBuffer = new ArrayBuffer(1024 * 1024 * 4);
await pixelMap.readPixelsToBuffer(readBuffer);
const imageArr = new Uint8Array(readBuffer);
// Normalization
let float32View = new Float32Array(1024 * 1024 * 3);
let means = [0.5, 0.5, 0.5];
let stds = [1.0, 1.0, 1.0];
let index = 0;
for (let i = 0; i < imageArr.length; i++) {
if ((i + 1) % 4 === 0) {
float32View[index] = (imageArr[i - 3] / 255.0 - means[0]) / stds[0];
float32View[index + 1] = (imageArr[i - 2] / 255.0 - means[1]) / stds[1];
float32View[index + 2] = (imageArr[i - 1] / 255.0 - means[2]) / stds[2];
index += 3;
}
}
// Release resources
pixelMap.release();
imageSource.release();
fileIo.closeSync(file.fd);
console.info('✅ [TaskPool] Preprocessing done');
return float32View.buffer;
}Main thread invocation:
// IDPhotoProcessor.ets
async function processWithTaskPool(imageUri: string) {
this.isProcessing = true;
try {
// 2. Execute concurrent task via taskpool.execute
// First param is concurrent function, rest are arguments
const inputBuffer = await taskpool.execute(preprocessImage, imageUri)
.then((result: Object) => {
console.info('✅ Preprocessing task completed');
return result as ArrayBuffer;
})
.catch((err: BusinessError) => {
console.error(`Preprocessing failed: ${err.message}`);
throw err;
});
// Proceed to AI inference...
await this.runInference(inputBuffer);
} catch (error) {
console.error('Processing failed:', error);
} finally {
this.isProcessing = false;
}
}Effect
After TaskPool refactor, main thread no longer blocks ! UI remains responsive for cancel, parameter adjustment, etc., while preprocessing runs in background.
Solution 2: Worker for Long-Running AI Inference
For AI inference exceeding 3 minutes, TaskPool is unsuitable because long tasks get reclaimed by system. Worker is required.
What is Worker?
Worker provides independent thread solution; developers create long-running Worker threads and manually control lifecycle. Suitable for resident or long-duration tasks.
Creating Worker
Worker file: entry/src/main/ets/workers/AIInferenceWorker.ets (omitted for brevity). Main thread usage:
// IDPhotoProcessor.ets
export class IDPhotoProcessor {
private workerInstance: worker.ThreadWorker | null = null;
private taskCallbacks: Map<string, { resolve: Function, reject: Function }> = new Map();
// Initialize Worker
async initWorker(): Promise<void> {
if (this.workerInstance) return;
return new Promise((resolve, reject) => {
// Create Worker instance
this.workerInstance = new worker.ThreadWorker(
'entry/ets/workers/AIInferenceWorker.ets',
{ name: 'AI Inference Worker' }
);
const taskId = 'init-' + Date.now();
this.taskCallbacks.set(taskId, { resolve, reject });
// Listen Worker messages
this.workerInstance.onmessage = (event: worker.MessageEvents) => {
const { type, data, error, taskId } = event.data;
const callback = this.taskCallbacks.get(taskId);
if (!callback) return;
if (type === 'initDone') {
callback.resolve();
this.taskCallbacks.delete(taskId);
} else if (type === 'error') {
callback.reject(new Error(error));
this.taskCallbacks.delete(taskId);
} else if (type === 'inferenceResult') {
callback.resolve(data);
this.taskCallbacks.delete(taskId);
}
};
// Load model file
const context = getContext(this) as common.UIAbilityContext;
const resMgr = context.resourceManager;
const modelBuffer = resMgr.getRawFileContentSync(MODEL_NAME);
// Send init message to Worker
this.workerInstance.postMessage({
type: 'init',
data: { modelBuffer: modelBuffer.buffer },
taskId
});
});
}
// Execute AI inference
async runInference(inputBuffer: ArrayBuffer): Promise<ArrayBuffer> {
if (!this.workerInstance) {
await this.initWorker();
}
return new Promise((resolve, reject) => {
const taskId = 'inference-' + Date.now() + '-' + Math.random();
this.taskCallbacks.set(taskId, { resolve, reject });
this.workerInstance!.postMessage({
type: 'inference',
data: { inputBuffer },
taskId
});
});
}
// Release Worker resources
releaseWorker(): void {
if (this.workerInstance) {
this.workerInstance.postMessage({ type: 'release' });
this.workerInstance.terminate();
this.workerInstance = null;
}
}
}Worker Advantages
Long tasks uninterrupted : Worker threads run continuously over 3 minutes, suitable for large model inference.
Model reuse : Model loaded once, reused for multiple inferences, avoiding repeated load overhead.
Task queue management : Can queue multiple inference requests.
Progress feedback : Can send progress updates to main thread during inference.
Solution 3: TaskGroup for Batch Photo Generation
Users often need multiple background colors (red, blue, white). Serial processing is slow; parallel needs task management.
What is TaskGroup?
TaskGroup is TaskPool's task group feature; multiple tasks added to a group, awaited uniformly.
Batch Implementation
Actual engineering implementation is more complex; recommend extracting independent utility class. Previous blog covers this in detail; below is pseudo-code for execution logic.
// BatchProcessor.ets
@Concurrent
async function generateColorBackground(
imageData: ArrayBuffer,
maskData: ArrayBuffer,
bgColor: [number, number, number]
): Promise<ArrayBuffer> {
console.info(`🎨 Generating ${bgColor} background ID photo`);
// Composite original image and segmentation mask into specified background color ID photo
const imageArr = new Uint8Array(imageData);
const maskArr = new Uint8Array(maskData);
const result = new Uint8Array(1024 * 1024 * 4);
for (let i = 0; i < result.length; i += 4) {
const maskAlpha = maskArr[i + 3] / 255.0;
if (maskAlpha > 0.5) {
// Portrait area retains original color
} else {
// Background area replaced with specified color
}
}
return result.buffer;
}
async function batchGenerateIDPhotos(
imageData: ArrayBuffer,
maskData: ArrayBuffer
): Promise<void> {
// Define background colors to generate
const bgColors: Array<[string, [number, number, number]]> = [
//.......
];
// Create task group
let taskGroup = new taskpool.TaskGroup();
let tasks: taskpool.Task[] = [];
// Create task for each background color
bgColors.forEach(([name, color]) => {
let task = new taskpool.Task(generateColorBackground, imageData, maskData, color);
tasks.push(task);
taskGroup.addTask(task);
console.info(`📋 Added task: ${name}`);
});
try {
// Execute task group, wait for all tasks
console.info('🚀 Starting batch generation...');
const results = await taskpool.execute(taskGroup) as ArrayBuffer[];
// Process results
results.forEach((result, index) => {
const [name] = bgColors[index];
console.info(`✅ ${name} generated`);
// Save or display result
this.saveResult(result, name);
});
console.info('🎉 All ID photos generated');
} catch (error) {
console.error('Batch generation failed:', error);
}
}TaskGroup Benefits
Unified wait : Single await waits for all tasks.
Parallel execution : Multiple tasks run simultaneously, greatly improving efficiency.
Ordered results : Returned result array matches task addition order.
Error handling : Any task failure fails the whole group, enabling unified handling.
Solution 4: Sequential Task Processing with SequenceRunner
Some scenarios require strict order: face detection → portrait segmentation → ID photo generation. These steps have dependencies and cannot run concurrently.
Using SequenceRunner for Serial Execution
// SerialProcessor.ets
import { taskpool } from '@kit.ArkTS';
@Concurrent
async function detectFace(imageUri: string): Promise<{ hasFace: boolean, faceRect?: Rect }> {
console.info('🔍 Detecting face...');
// Face detection logic
return { hasFace: true, faceRect: { x: 100, y: 100, width: 200, height: 200 } };
}
@Concurrent
async function segmentPortrait(imageUri: string, faceRect: Rect): Promise<ArrayBuffer> {
console.info('✂️ Portrait segmentation...');
// Segmentation logic
return new ArrayBuffer(1024 * 1024 * 4);
}
@Concurrent
async function generateIDPhotoFromMask(imageUri: string, mask: ArrayBuffer, bgColor: string): Promise<ArrayBuffer> {
console.info('📸 Generating ID photo...');
// Generation logic
return new ArrayBuffer(1024 * 1024 * 4);
}
async function processIDPhotoSerial(imageUri: string, bgColor: string): Promise<void> {
// Create serial executor
const runner = new taskpool.SequenceRunner();
// Create tasks
const faceTask = new taskpool.Task(detectFace, imageUri);
let faceRect: Rect | undefined;
// Execute tasks in order
try {
// Step 1: Face detection
await runner.execute(faceTask).then((result: any) => {
if (!result.hasFace) {
throw new Error('No face detected');
}
faceRect = result.faceRect;
console.info('✅ Face detection done');
});
// Step 2: Portrait segmentation (depends on face detection)
const segmentTask = new taskpool.Task(segmentPortrait, imageUri, faceRect);
let mask: ArrayBuffer;
await runner.execute(segmentTask).then((result: ArrayBuffer) => {
mask = result;
console.info('✅ Portrait segmentation done');
});
// Step 3: Generate ID photo (depends on segmentation)
const generateTask = new taskpool.Task(generateIDPhotoFromMask, imageUri, mask, bgColor);
await runner.execute(generateTask).then((result: ArrayBuffer) => {
console.info('✅ ID photo generation done');
this.showResult(result);
});
} catch (error) {
console.error('Processing failed:', error);
}
}Solution 5: Inter-Thread Communication Optimization
Threads exchange large data: image data, segmentation masks. Unoptimized large-object copying becomes bottleneck.
Using SharedArrayBuffer for Shared Memory
// SharedMemoryProcessor.ets
import { taskpool } from '@kit.ArkTS';
@Concurrent
function processWithSharedMemory(sharedBuffer: SharedArrayBuffer, width: number, height: number): void {
// Wrap SharedArrayBuffer as Uint8Array for manipulation
const pixels = new Uint8Array(sharedBuffer);
// Operate directly on shared memory, no copy needed
for (let i = 0; i < pixels.length; i += 4) {
// Process pixel...
// Changes reflect directly in shared memory
}
console.info('✅ Shared memory processing done');
}
async function useSharedMemory(imageUri: string): Promise<void> {
// Load image
let pixelMap = await loadImage(imageUri);
const { width, height } = pixelMap.getImageInfoSync();
// Create shared memory
const sharedBuffer = new SharedArrayBuffer(width * height * 4);
let buffer = new Uint8Array(sharedBuffer);
// Read pixel data into shared memory
await pixelMap.readPixelsToBuffer(sharedBuffer);
// Pass shared memory to TaskPool task
const task = new taskpool.Task(processWithSharedMemory, sharedBuffer, width, height);
await taskpool.execute(task);
// Read processed data directly from shared memory
const processedPixels = new Uint8Array(sharedBuffer);
// Create new PixelMap for display
const resultPixelMap = await image.createPixelMapFromData(processedPixels, {
size: { width, height }
});
this.showResult(resultPixelMap);
}Using Sendable Objects to Share Model Instances
For complex objects shared across threads, define as Sendable class:
// sendable/ModelLoader.ets
"use shared"
@Sendable
export class ModelLoader {
private static instance: ModelLoader;
private modelBuffer: ArrayBuffer | null = null;
private constructor() {}
public static getInstance(): ModelLoader {
if (!ModelLoader.instance) {
ModelLoader.instance = new ModelLoader();
}
return ModelLoader.instance;
}
public async loadModel(context: common.UIAbilityContext): Promise<void> {
if (this.modelBuffer) return;
const resMgr = context.resourceManager;
this.modelBuffer = (await resMgr.getRawFileContent(MODEL_NAME)).buffer;
console.info('✅ Model loaded');
}
public getModelBuffer(): ArrayBuffer {
if (!this.modelBuffer) {
throw new Error('Model not loaded');
}
return this.modelBuffer;
}
}Usage in multiple threads:
// Main thread
import { ModelLoader } from './sendable/ModelLoader';
async function initModel() {
const loader = ModelLoader.getInstance();
await loader.loadModel(getContext(this));
}
// Worker thread
import { ModelLoader } from '../sendable/ModelLoader';
import { worker } from '@kit.ArkTS';
const workerPort = worker.workerPort;
workerPort.onmessage = async () => {
// Directly get singleton instance
const loader = ModelLoader.getInstance();
const modelBuffer = loader.getModelBuffer();
// Use model...
};Solution 6: Producer-Consumer Pattern for Preview Queue
Real-time preview needs efficient producer-consumer for video frames.
// PreviewProcessor.ets
import { taskpool } from '@kit.ArkTS';
// Frame queue
class FrameQueue {
private queue: Array<{ frameData: ArrayBuffer, timestamp: number }> = [];
private maxSize: number = 5;
private processing = false;
// Producer: add frame
async addFrame(frameData: ArrayBuffer, timestamp: number): Promise<void> {
if (this.queue.length >= this.maxSize) {
// Drop oldest frame when queue full
this.queue.shift();
}
this.queue.push({ frameData, timestamp });
if (!this.processing) {
this.processQueue();
}
}
// Consumer: process queue
private async processQueue(): Promise<void> {
if (this.queue.length === 0) {
this.processing = false;
return;
}
this.processing = true;
const frame = this.queue.shift()!;
try {
// Delegate frame processing to TaskPool
const result = await taskpool.execute(processFrame, frame.frameData);
// Update preview on main thread
this.updatePreview(result, frame.timestamp);
} catch (error) {
console.error('Frame processing failed:', error);
}
// Continue with next frame
this.processQueue();
}
@Concurrent
private static async processFrame(frameData: ArrayBuffer): Promise<ArrayBuffer> {
// Lightweight inference (smaller model or lower resolution)
// ...
return processedData;
}
}
// Camera preview usage
class CameraPreview {
private frameQueue = new FrameQueue();
// Camera frame callback (high frequency)
onFrame(frameData: ArrayBuffer, timestamp: number): void {
// Enqueue directly, non-blocking
this.frameQueue.addFrame(frameData, timestamp);
}
}Performance Comparison & Best Practices
Before/After Performance
Image Preprocessing : Before 800ms (UI lag) → After 850ms (no lag) → Improvement: UI fluid
AI Inference : Before 2500ms (frozen) → After 2600ms (cancellable) → Improvement: Interactive
Batch Generate 3 Photos : Before 7.5s (serial) → After 2.8s (parallel) → Improvement: 168%
Memory Peak : Before 150MB → After 180MB (acceptable) → Note: Shared memory optimization limited actual memory growth.
Concurrency Selection Guide
Image preprocessing (<3 min) → TaskPool: Auto-managed, low overhead
AI model inference (>3 min) → Worker: Resident thread, model reuse
Batch generate multiple photos → TaskGroup: Parallel execution, unified wait
Dependent tasks → SequenceRunner: Guarantees execution order
Real-time preview frame processing → Producer-consumer queue: Load control, no frame drops
Large object sharing → SharedArrayBuffer: Avoids copy overhead
Singleton sharing → Sendable object: Thread-safe, auto-sync
Best Practices Summary
Async whenever possible : Move all time-consuming ops to background threads.
Reuse whenever possible : Worker threads, model instances, Sendable singletons.
Share whenever possible : Use SharedArrayBuffer to reduce copies.
Control whenever possible : Use task groups, serial queues to manage flow.
Drop when necessary : For real-time preview, discard old frames to maintain fluidity.
Conclusion & Outlook
Through the AI ID photo tool case, we systematically learned HarmonyOS concurrency design:
TaskPool : Independent time-consuming tasks like image preprocessing.
Worker : Long tasks like AI inference.
TaskGroup : Batch parallel processing.
SequenceRunner : Serial dependent tasks.
SharedArrayBuffer : Large object sharing optimization.
Sendable : Thread-safe singleton.
Producer-consumer : Real-time preview handling.
Future exploration: NPU hardware acceleration for AI inference, complex multimodal input processing, distributed device collaborative computing. On-device AI + efficient concurrency makes mobile apps smarter and smoother.
References
TaskPool API Reference: https://developer.huawei.com/consumer/cn/doc/harmonyos-references/js-apis-taskpool
Worker API Reference: https://developer.huawei.com/consumer/cn/doc/harmonyos-references/js-apis-worker
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
51CTO HarmonyOS Developer Community
The HarmonyOS Developer Community is a learning-oriented community for developers to learn, communicate, ask questions, and share.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
