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:
- performing asynchronous I/O (e.g., writing multiple
ArrayBuffers to the same file) but guaranteeing that each buffer is appended strictly in sequence; and - 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)
}
How it works (briefly):
-
@ConcurrentmarkswriteBufto run off the UI thread. - Each call to
pool.write()wrapswriteBufin ataskpool.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;
}
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
Key Takeaways
-
Default
TaskPoolexecution is not ordered. UseSequenceRunnerto 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
@Concurrentand interact with the UI only from the host thread. -
Deterministic I/O is achievable. Combining
SequenceRunner(for serialized flows) andaddDependency(for DAG-like workflows) gives both correctness and performance under a managed thread pool.
Top comments (0)