DEV Community

HarmonyOS
HarmonyOS

Posted on

TaskPool ensures that multiple concurrent tasks are executed in sequence

Read the original article:TaskPool ensures that multiple concurrent tasks are executed in sequence

Context

You need to run multiple concurrent tasks in HarmonyOS/ArkTS while still enforcing a specific execution order. Two common cases:

  1. performing asynchronous I/O (e.g., writing multiple ArrayBuffers to the same file) but guaranteeing that each buffer is appended strictly in sequence; and
  2. orchestrating tasks that have explicit dependencies so they execute in a defined order even when dispatched concurrently.

Description

HarmonyOS’s TaskPool provides a managed multithreading environment: you submit tasks from the host thread, the system schedules them on worker threads, and returns results to the host. It supports execution, cancellation, and priorities with unified thread management, dynamic scheduling, and load balancing.

For strictly ordered execution, there are two key tools:

  • SequenceRunner: represents a serial queue to run a set of tasks one-by-one in creation order.
  • addDependency: allows a task to depend on another. Important constraints: the current task and its dependency cannot be group tasks, serial-queue tasks, async-queue tasks, already-executed tasks, or periodic tasks. Tasks involved in dependencies (dependents or dependees) cannot be executed again after completion.

Solution / Approach

Problem 1: Asynchronous I/O with ordered file appends

TaskPool.execute() queues tasks that may run out of order. To force sequential appends (write the next ArrayBuffer only after the previous write completes), create tasks and submit them to a SequenceRunner, which guarantees FIFO execution.

@Concurrent
function writeBuf(filePath: string, buf: ArrayBuffer) {
  let start: number = new Date().getTime();
  while (new Date().getTime() - start < (buf.byteLength * 100)) {
    continue;
  }
  let file = fs.openSync(filePath, fs.OpenMode.READ_WRITE | fs.OpenMode.CREATE);
  let stat = fs.statSync(file.fd)
  fs.writeSync(file.fd, buf, { offset: stat.size });
  fs.closeSync(file);
}

export class SerialTaskPool {
  private group: taskpool.SequenceRunner = new taskpool.SequenceRunner();
  write(path: string, buff: ArrayBuffer) {
    console.info(`Demo enter write:${path}  length:${buff.byteLength}`)
    let task = new taskpool.Task(writeBuf, path, buff)
    this.group.execute(task)
  }
}

aboutToAppear(): void {
  let pool: SerialTaskPool = new SerialTaskPool()
  let filePath = getContext(this).filesDir + '/' + 'test.txt'
  let arrrayBuff0 = new Uint8Array([1, 2, 3, 4, 5]).buffer as ArrayBuffer
  let arrrayBuff1 = new Uint8Array([6, 7, 8]).buffer as ArrayBuffer
  let arrrayBuff2 = new Uint8Array([9, 0]).buffer as ArrayBuffer
  pool.write(filePath, arrrayBuff0)
  pool.write(filePath, arrrayBuff1)
  pool.write(filePath, arrrayBuff2)
}
Enter fullscreen mode Exit fullscreen mode

How it works (briefly):

  • @Concurrent marks writeBuf to run off the UI thread.
  • Each call to pool.write() wraps writeBuf in a taskpool.Task.
  • SequenceRunner.execute() enqueues tasks so that each write happens only after the previous one finishes, ensuring on-disk order.

Problem 2: Enforcing a dependency chain among tasks

When tasks depend on each other (e.g., task3 → task2 → task1) and must execute in that order, use addDependency to build the dependency graph before executing. Then dispatch each task; the system honors the dependencies.

import { taskpool } from '@kit.ArkTS';

@Component
@Entry
struct TaskPoolDemo {
  build() {
    Column() {
      Text('TaskTool Multi-Task')
    }
  }

  aboutToAppear(): void {
    let task1: taskpool.Task = new taskpool.Task(delay, 100);
    let task2: taskpool.Task = new taskpool.Task(delay, 200);
    let task3: taskpool.Task = new taskpool.Task(delay, 200);


    console.info("dependency: add dependency start");
    task1.addDependency(task2);
    task2.addDependency(task3);
    console.info("dependency: add dependency end");


    console.info("dependency: start execute second");
    taskpool.execute(task1).then(() => {
      console.info("dependency: second task1 success");
    })
    taskpool.execute(task2).then(() => {
      console.info("dependency: second task2 success");
    })
    taskpool.execute(task3).then(() => {
      console.info("dependency: second task3 success");
    })
  }
}

@Concurrent
function delay(args: number): number {
  let t: number = Date.now();
  while ((Date.now() - t) < 1000) {
    continue;
  }
  return args;
}
Enter fullscreen mode Exit fullscreen mode

Expected logs confirming the order:

06-23 17:34:24.694         dependency: add dependency start
06-23 17:34:24.694         dependency: add dependency end
06-23 17:34:24.694         dependency: start execute second
06-23 17:34:25.990         dependency: second task3 success
06-23 17:34:26.990         dependency: second task2 success
06-23 17:34:27.991         dependency: second task1 success
Enter fullscreen mode Exit fullscreen mode

Key Takeaways

  • Default TaskPool execution is not ordered. Use SequenceRunner to enforce strict FIFO for scenarios like sequential file appends.
  • Model dependencies explicitly. With addDependency, define a chain (e.g., task1.addDependency(task2)) so tasks run only when prerequisites finish.
  • Respect constraints. Dependent tasks cannot be group/serial/async-queue tasks, previously executed tasks, or periodic tasks, and tasks with dependencies are not re-executable after completion.
  • Keep heavy work off the UI thread. Mark worker functions with @Concurrent and interact with the UI only from the host thread.
  • Deterministic I/O is achievable. Combining SequenceRunner (for serialized flows) and addDependency (for DAG-like workflows) gives both correctness and performance under a managed thread pool.

Written by Fatih Turan Gundogdu

Top comments (0)