jsrosetta

Bất đồng bộ

Huỷ tác vụ: AbortController và context

AbortController/AbortSignal của Node.js so với context.Context của Go: huỷ thủ công, timeout, gộp nhiều tín hiệu huỷ và tự kiểm tra việc huỷ trong vòng lặp.

Tác giả:
Phiên bản tối thiểu
Node.js ≥ 20.3Go ≥ 1.21
Đã chạy thử trên
Node.js 24.12.0Go 1.27.1

Code Node.js là ES module: lưu file .mjs hoặc đặt "type": "module" trong package.json.

Một promise đã chạy thì không có cách nào "dừng" nó từ bên ngoài — Node.js giải quyết bằng một tín hiệu đi kèm: AbortController tạo ra một AbortSignal, ai muốn huỷ thì gọi controller.abort(), còn hàm nhận signal tự lắng nghe và dừng sớm. Go có context.Context đóng đúng vai trò đó: ctx.Done() là một channel bị đóng khi context bị huỷ, ctx.Err() cho biết lý do. Cả hai đều là cơ chế hợp tác (cooperative): không có gì bị "giết" cưỡng bức, code đang chạy phải tự kiểm tra tín hiệu và tự dừng. Khác biệt lớn nhất nằm ở cách lan truyền: context của Go được truyền tường minh làm tham số đầu tiên xuống toàn bộ cây lời gọi, và mỗi context con tự bị huỷ khi context cha bị huỷ; AbortSignal thì là một đối tượng độc lập, muốn gộp nhiều tín hiệu phải dùng AbortSignal.any().

Huỷ thủ công (abort / cancel)

import { setTimeout as sleep } from "node:timers/promises";
 
const controller = new AbortController();
const { signal } = controller;
 
signal.addEventListener("abort", () => {
  console.log("abort event:", signal.reason.message); // → abort event: user cancelled
}, { once: true });
 
// huỷ sau 100ms, trong khi công việc cần tới 1s
setTimeout(() => controller.abort(new Error("user cancelled")), 100);
 
try {
  await sleep(1000, "done", { signal });
} catch (err) {
  console.log(err.name);          // → AbortError
  console.log(err.cause.message); // → user cancelled
}
 
console.log(signal.aborted);        // → true
console.log(signal.reason.message); // → user cancelled

Timeout (AbortSignal.timeout / context.WithTimeout)

import { setTimeout as sleep } from "node:timers/promises";
 
const signal = AbortSignal.timeout(100); // tự abort sau 100ms
 
try {
  await sleep(1000, "done", { signal });
} catch (err) {
  console.log(err.name);           // → AbortError
  console.log(err.cause.name);     // → TimeoutError
  console.log(signal.reason.name); // → TimeoutError
}

Gộp nhiều tín hiệu huỷ (AbortSignal.any / context con)

import { setTimeout as sleep } from "node:timers/promises";
 
async function download(userDelay) {
  const user = new AbortController();
  // abort khi người dùng huỷ HOẶC khi quá 200ms, cái nào tới trước
  const signal = AbortSignal.any([user.signal, AbortSignal.timeout(200)]);
 
  const timer = setTimeout(() => user.abort(new Error("user cancelled")), userDelay);
  try {
    return await sleep(1000, "done", { signal });
  } catch {
    return `aborted: ${signal.reason.name}: ${signal.reason.message}`;
  } finally {
    clearTimeout(timer);
  }
}
 
console.log(await download(50));  // → aborted: Error: user cancelled
console.log(await download(500)); // → aborted: TimeoutError: The operation was aborted due to timeout

Tự kiểm tra việc huỷ trong vòng lặp (throwIfAborted / ctx.Err)

import { setTimeout as sleep } from "node:timers/promises";
 
async function processItems(items, signal) {
  for (const item of items) {
    signal.throwIfAborted(); // ném signal.reason nếu đã bị abort
    await sleep(50);         // giả lập xử lý một item
    console.log("processed", item);
  }
}
 
try {
  await processItems([1, 2, 3, 4, 5], AbortSignal.timeout(125));
} catch (err) {
  console.log("stopped:", err.name);
}
// → processed 1
// → processed 2
// → processed 3
// → stopped: TimeoutError

Khác biệt chính

Node.js Go
Tạo tín hiệu huỷ new AbortController() context.WithCancel / WithCancelCause
Huỷ controller.abort(reason) cancel() / cancel(cause)
Timeout AbortSignal.timeout(ms) context.WithTimeout(parent, d)
Lý do huỷ signal.reason ctx.Err() + context.Cause(ctx)
Kiểm tra trong vòng lặp signal.throwIfAborted() ctx.Err() != nil
Gộp nhiều tín hiệu AbortSignal.any([...]) context con (cha huỷ → con huỷ), context.AfterFunc
Lan truyền truyền { signal } khi cần truyền ctx làm tham số đầu tiên xuyên suốt cây lời gọi