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.
- 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 cancelledpackage main
import (
"context"
"errors"
"fmt"
"time"
)
// sleep chờ d, hoặc trả về sớm khi ctx bị huỷ — giống setTimeout của
// node:timers/promises với { signal }.
func sleep(ctx context.Context, d time.Duration) error {
timer := time.NewTimer(d)
defer timer.Stop()
select {
case <-timer.C:
return nil
case <-ctx.Done():
return ctx.Err()
}
}
func main() {
ctx, cancel := context.WithCancelCause(context.Background())
defer cancel(nil) // luôn gọi cancel để giải phóng tài nguyên của ctx
// huỷ sau 100ms, trong khi công việc cần tới 1s
time.AfterFunc(100*time.Millisecond, func() {
cancel(errors.New("user cancelled"))
})
if err := sleep(ctx, 1*time.Second); err != nil {
fmt.Println(err) // → context canceled
fmt.Println(errors.Is(err, context.Canceled)) // → true
fmt.Println(context.Cause(ctx)) // → 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
}package main
import (
"context"
"errors"
"fmt"
"time"
)
func sleep(ctx context.Context, d time.Duration) error {
timer := time.NewTimer(d)
defer timer.Stop()
select {
case <-timer.C:
return nil
case <-ctx.Done():
return ctx.Err()
}
}
func main() {
ctx, cancel := context.WithTimeout(context.Background(), 100*time.Millisecond) // tự huỷ sau 100ms
defer cancel()
err := sleep(ctx, 1*time.Second)
fmt.Println(err) // → context deadline exceeded
fmt.Println(errors.Is(err, context.DeadlineExceeded)) // → true
fmt.Println(errors.Is(err, context.Canceled)) // → false
}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 timeoutpackage main
import (
"context"
"errors"
"fmt"
"time"
)
func sleep(ctx context.Context, d time.Duration) error {
timer := time.NewTimer(d)
defer timer.Stop()
select {
case <-timer.C:
return nil
case <-ctx.Done():
return ctx.Err()
}
}
func download(userDelay time.Duration) string {
user, cancelUser := context.WithCancelCause(context.Background())
defer cancelUser(nil)
// context con: bị huỷ khi cha (user) bị huỷ HOẶC khi quá 200ms
ctx, cancel := context.WithTimeout(user, 200*time.Millisecond)
defer cancel()
timer := time.AfterFunc(userDelay, func() { cancelUser(errors.New("user cancelled")) })
defer timer.Stop()
if err := sleep(ctx, 1*time.Second); err != nil {
return fmt.Sprint("aborted: ", context.Cause(ctx))
}
return "done"
}
func main() {
fmt.Println(download(50 * time.Millisecond)) // → aborted: user cancelled
fmt.Println(download(500 * time.Millisecond)) // → aborted: context deadline exceeded
// hai context KHÔNG có quan hệ cha-con (vd. shutdown của server và một
// request): nối chúng bằng context.AfterFunc
shutdown, stopServer := context.WithCancelCause(context.Background())
request, cancelRequest := context.WithCancelCause(context.Background())
defer cancelRequest(nil)
stop := context.AfterFunc(shutdown, func() {
cancelRequest(context.Cause(shutdown))
})
defer stop()
stopServer(errors.New("server shutting down"))
<-request.Done()
fmt.Println(context.Cause(request)) // → server shutting down
}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: TimeoutErrorpackage main
import (
"context"
"fmt"
"time"
)
func processItems(ctx context.Context, items []int) error {
for _, item := range items {
if err := ctx.Err(); err != nil { // khác nil nếu ctx đã bị huỷ, ~ throwIfAborted()
return err
}
time.Sleep(50 * time.Millisecond) // giả lập xử lý một item
fmt.Println("processed", item)
}
return nil
}
func main() {
ctx, cancel := context.WithTimeout(context.Background(), 125*time.Millisecond)
defer cancel()
if err := processItems(ctx, []int{1, 2, 3, 4, 5}); err != nil {
fmt.Println("stopped:", err)
}
}
// → processed 1
// → processed 2
// → processed 3
// → stopped: context deadline exceededKhá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 |