Mobile Development 29 min read

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.

51CTO HarmonyOS Developer Community
51CTO HarmonyOS Developer Community
51CTO HarmonyOS Developer Community
Mastering HarmonyOS Concurrency: TaskPool, Worker & TaskGroup in AI Photo App

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.

HarmonyOS concurrency architecture diagram
HarmonyOS concurrency architecture diagram

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

Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

ConcurrencyHarmonyOSWorkerSharedArrayBufferTaskPoolTaskGroupSendableSequenceRunner
51CTO HarmonyOS Developer Community
Written by

51CTO HarmonyOS Developer Community

The HarmonyOS Developer Community is a learning-oriented community for developers to learn, communicate, ask questions, and share.

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.