DrillLab
第 14 / 21 节LESSON 14 / 21约 16 分钟~16 min

实现:worker pool(工人池)Building it: a worker pool, meaning a fixed number of workers sharing one queue

别想复杂了。就是「开 limit 个工人,一起从同一个待办队列里抢活」。Do not overthink it. You start limit workers, and they all take jobs from the same to-do queue.

3 个练习3 exercisesReact · 第 4 部分React · Part 4
这一页有什么On this page9
学完这节你会After this lesson you can
  • 独立实现 runTasks,并解释每一行为什么这么写Implement runTasks without help, and explain why each line is written that way
  • 说清「共享游标」为什么天然保证了并发上限Explain why one shared cursor already guarantees the concurrency limit
  • 说清「按下标写回」为什么天然保证了顺序Explain why writing results back by index already guarantees the order
  • 会读 npm run q2 的输出并判断实现是否正确Read the output of npm run q2 and decide whether the implementation is correct
这在考试里考什么What the exam does with this

这是 Q2 的完整答案。而且 worker pool 是一个可迁移的模式 —— 任何「限制并发」的题都是这个骨架。This is the full answer to Q2. The worker pool is also a pattern you can carry to other problems: every question about limiting concurrency has this same skeleton.

这节课要看的真实文件Real files this lesson looks at高亮行 = 需要你动手改的文件Highlighted rows are the files you edit
react-notes-app/q2/taskRunner.ts要实现的 runTasksThe runTasks you have to write

提醒:源项目在磁盘上是做完的版本 —— 下面就是答案。想自己先写一遍的话,现在关上。Heads up: on disk this project is the finished version — what follows is the answer. Close this if you want to write it yourself first.

TypeScripttaskRunner.ts源项目From source
1// Q2: Implement a custom asynchronous task runner.
2//
3// Requirements:
4// 1. `tasks` is an array of FUNCTIONS. Each function, when called,
5// starts an async job and returns a Promise.
6// 2. At most `limit` tasks may be RUNNING at the same time.
7// A new task may only start after one of the running tasks finishes.
8// 3. The runner NEVER throws, even if some tasks reject.
9// It resolves with an array of results IN THE SAME ORDER as `tasks`:
10// { status: "fulfilled", value: T } for tasks that succeeded
11// { status: "rejected", reason: unknown } for tasks that failed
12// (This mimics Promise.allSettled, but with a concurrency throttle.)
13
14export type Task<T> = () => Promise<T>;
15
16export type SettledResult<T> =
17 | { status: "fulfilled"; value: T }
18 | { status: "rejected"; reason: unknown };
19
20export async function runTasks<T>(
21 tasks: Task<T>[],
22 limit: number,
23): Promise<SettledResult<T>[]> {
24 // TODO: implement me
25 const results: SettledResult<T>[] = new Array(tasks.length);
26
27 let nextIndex = 0;
28
29 const worker = async () => {
30 while (nextIndex < tasks.length) {
31 const i = nextIndex;
32 nextIndex++;
33
34 try {
35 const value = await tasks[i]();
36 results[i] = { status: "fulfilled", value };
37 } catch (reason) {
38 results[i] = { status: "rejected", reason };
39 }
40 }
41 };
42 if (tasks.length === 0) return [];
43
44 const workerCount = Math.min(limit, tasks.length);
45 const workers: Promise<void>[] = [];
46 for (let w = 0; w < workerCount; w++) {
47 workers.push(worker());
48 }
49 await Promise.all(workers);
50 return results;
51}
Source: react-notes-app/q2/taskRunner.ts
§01

先排除一个直觉上的错解:分批First rule out the answer that feels obvious: fixed batches

「6 个任务、上限 2,那就切成 3 批」—— 这个想法能跑,但不对。Six tasks, a limit of 2, so cut them into 3 batches. That idea runs, but it is wrong.

分批的写法是:取前 2 个跑完,再取接下来 2 个,再取最后 2 个。 并发数确实不会超过 2。

问题在于它浪费时间。每一批都要等那一批里最慢的结束, 才能开始下一批。用 demo 里的真实耗时算一下:

分批要 650ms,worker pool 只要 450ms。 更重要的是它违反了题目原文:A new task may only start after one of the running tasks finishes —— 「其中一个结束就能开下一个」, 不是「这一批都结束」。

所以要的不是分批,是「谁空了谁接着干」

Batching means: take the first 2 and wait for both, then the next 2, then the last 2. Concurrency really never goes above 2.

The problem is that it wastes time. Every batch has to wait for the slowest task in that batch before the next batch can start. Do the arithmetic with the real durations from the demo:

Batching needs 650ms; the worker pool needs only 450ms. More importantly it breaks the wording of the brief: A new task may only start after one of the running tasks finishes — “one of them finishing lets the next one start”, not “the whole batch finishes”.

So what you want is not batching, it is “whoever is free takes the next job”.

Text两种思路的耗时对比How long the two approaches take示意Illustrative
1任务耗时:1→300ms 2→100ms 3→200ms 4→100ms 5→150ms 6→100ms
2
3✗ 分批(每批等最慢的)
4 批1: [1(300), 2(100)] → 等 300ms
5 批2: [3(200), 4(100)] → 等 200ms
6 批3: [5(150), 6(100)] → 等 150ms
7 总计 650ms,而且中间有大量空闲槽位
8
9✓ worker pool(谁空了谁补上)
10 t=0 开 1、2
11 t=100 2 完 → 立刻开 3
12 t=300 1 完 → 立刻开 4 3 还在跑
13 t=300 3 完 → 立刻开 5
14 t=400 4 完 → 立刻开 6
15 t=450 5、6 陆续完
16 总计 ~450ms,槽位几乎没空过
1Task durations: 1→300ms 2→100ms 3→200ms 4→100ms 5→150ms 6→100ms
2
3✗ Batching (each batch waits for its slowest task)
4 batch 1: [1(300), 2(100)] → wait 300ms
5 batch 2: [3(200), 4(100)] → wait 200ms
6 batch 3: [5(150), 6(100)] → wait 150ms
7 650ms in total, and many slots sit idle in between
8
9✓ worker pool (whoever is free takes the next job)
10 t=0 start 1 and 2
11 t=100 2 done → start 3 at once
12 t=300 1 done → start 4 at once 3 still running
13 t=300 3 done → start 5 at once
14 t=400 4 done → start 6 at once
15 t=450 5 and 6 finish one after the other
16 ~450ms in total, and a slot is almost never idle
§02

worker pool 的四个零件The four parts of a worker pool

拆开看,一共只有四样东西。Taken apart, there are only four things.

  1. 一个预分配的结果数组 new Array(tasks.length)。 长度一开始就定好,之后按下标填 ——这就是顺序保证的全部秘密, 不需要任何排序。
  2. 一个共享游标 let nextIndex = 0。 它记录「下一个该做的是第几号任务」。 所有 worker 共用这一个变量(靠闭包共享)。
  3. 一个 worker 函数。 它是个循环:抢一个任务号 → 游标 +1 → 跑它 → 结果写回对应下标 → 回到循环开头再抢。 队列空了就退出。
  4. 启动 limit 个 worker, 然后 await Promise.all 等它们全收工。同时存在的 worker 只有 limit 个, 所以同时在跑的任务也只有 limit 个 —— 并发上限是这么来的,不需要任何计数器。

这两个「天然保证」是这个解法漂亮的地方:并发上限来自「worker 的个数」, 顺序来自「按下标写回」。 两件难事都不需要额外代码。

  1. A pre-allocated result array new Array(tasks.length). The length is fixed up front and you fill it by index afterwards —that is the entire secret of the ordering guarantee, no sorting anywhere.
  2. A shared cursor let nextIndex = 0. It records which task number comes next. Every worker shares this one variable, through the closure.
  3. A worker function. It is a loop: grab a task number → bump the cursor → run the task → write the result back at that index → loop around and grab again. When the queue is empty it exits.
  4. Start limit workers, then await Promise.all until they all clock out.Only limit workers exist at a time, so only limit tasks run at a time — that is where the concurrency cap comes from, and it needs no counter at all.

Those two free guarantees are what makes this solution pretty: the concurrency cap comes from the number of workers, the ordering comes from writing back by index. Neither hard part needs extra code.

§03

游标不会被抢乱吗Can two workers grab the same cursor value?

不会。JavaScript 是单线程的。No. JavaScript runs on one thread.

看这两行:

在多线程语言里,两个线程可能同时读到 nextIndex = 3, 然后都去做第 3 号任务 —— 这叫竞态条件,得加锁。

JavaScript 是单线程的。 只有遇到 await 时才会把控制权交出去。const i = nextIndex; nextIndex++; 这两行中间没有 await,所以它们是一口气执行完的, 不可能被打断。

所以不需要锁。这是 JavaScript 并发模型的一个 实实在在的好处,也是这道题能写得这么短的原因。

Look at these two lines:

In a multi-threaded language two threads could read nextIndex = 3 at the same moment and both go do task 3 — that is a race condition, and it needs a lock.

But JavaScript is single-threaded. It only hands control away when it hits an await. There is no await between const i = nextIndex; nextIndex++;, so those two lines run in one breath and cannot be interrupted.

So no lock is needed. This is a real, concrete benefit of the JavaScript concurrency model, and the reason this task can be written so short.

TypeScript源项目From source
1const i = nextIndex; // 记下我抢到的号
2nextIndex++; // 立刻把游标推进,别人抢不到同一个
3 // ↑ 这两行之间没有 await,不会被打断
1const i = nextIndex; // remember the number I grabbed
2nextIndex++; // move the cursor at once so nobody grabs the same one
3 // ↑ no await between these two lines, so nothing interrupts them
Source: react-notes-app/q2/taskRunner.ts
§04

分步写出来Writing it one step at a time

第一步:结果数组和游标。

第二步:worker 的循环体。注意 await tasks[i]() —— 那对括号不能少,tasks[i] 是函数,要调用它才产生 Promise。try/catch 把失败接住写成 rejected然后循环继续 —— 这就是「NEVER throws」的实现方式。

第三步:启动 worker 并等待。Math.min(limit, tasks.length) 是个细节: 3 个任务、上限 10,只需要开 3 个 worker, 多开的会立刻发现队列空了然后退出 —— 没坏处,但没必要。

第四步:空数组早退。tasks.length === 0 时直接返回 []。 其实不加也对(0 个 worker,Promise.all([]) 立刻 resolve, 返回空数组),但显式写出来更清楚。

Step one: the result array and the cursor.

Step two: the body of the worker loop. Watch await tasks[i]() — that pair of parentheses is not optional. tasks[i] is a function; calling it is what produces a Promise. try/catch catches the failure and writes it as rejected, and then the loop keeps going — that is how “NEVER throws” gets implemented.

Step three: start the workers and wait.Math.min(limit, tasks.length) is a small detail: 3 tasks with a cap of 10 only needs 3 workers, and any extras would immediately find the queue empty and exit — harmless, but pointless.

Step four: the early return for an empty array. When tasks.length === 0, return [] straight away. Leaving it out is also correct (0 workers, Promise.all([]) resolves instantly, empty array back), but writing it out says so plainly.

TypeScript推导 · 第一步Working it out · step one示意Illustrative
1// 第一步
2const results: SettledResult<T>[] = new Array(tasks.length);
3let nextIndex = 0;
1// Step one
2const results: SettledResult<T>[] = new Array(tasks.length);
3let nextIndex = 0;
TypeScript推导 · 第二步Working it out · step two示意Illustrative
1// 第二步:一个 worker 不停地抢活
2const worker = async () => {
3 while (nextIndex < tasks.length) {
4 const i = nextIndex;
5 nextIndex++;
6
7 try {
8 const value = await tasks[i](); // ← 括号!调用它才开始跑
9 results[i] = { status: "fulfilled", value };
10 } catch (reason) {
11 results[i] = { status: "rejected", reason };
12 }
13 // catch 之后不 return,循环继续 → 一个失败不连累别人
14 }
15};
1// Step two: one worker keeps taking the next job
2const worker = async () => {
3 while (nextIndex < tasks.length) {
4 const i = nextIndex;
5 nextIndex++;
6
7 try {
8 const value = await tasks[i](); // ← the parentheses! calling it is what starts it
9 results[i] = { status: "fulfilled", value };
10 } catch (reason) {
11 results[i] = { status: "rejected", reason };
12 }
13 // no return after catch, the loop keeps going → one failure does not stop the others
14 }
15};
TypeScript推导 · 第三步Working it out · step three示意Illustrative
1// 第三步:开 limit 个 worker,等它们全收工
2const workerCount = Math.min(limit, tasks.length);
3const workers: Promise<void>[] = [];
4for (let w = 0; w < workerCount; w++) {
5 workers.push(worker()); // 这里的括号是「启动这个 worker」
6}
7await Promise.all(workers);
8return results;
1// Step three: start limit workers and wait for all of them
2const workerCount = Math.min(limit, tasks.length);
3const workers: Promise<void>[] = [];
4for (let w = 0; w < workerCount; w++) {
5 workers.push(worker()); // these parentheses mean "start this worker"
6}
7await Promise.all(workers);
8return results;
§05

完整答案The complete answer

这就是项目里的实现,已实测跑通。This is the implementation in the project, and it has been run and checked.

把四步拼起来。这份代码和 react-notes-app/q2/taskRunner.ts里的实现完全一致,npm run q2 实测通过三条验收标准。

Put the four steps together. This code is identical to the implementation in react-notes-app/q2/taskRunner.ts, and npm run q2 was run here and met all three acceptance criteria.

TypeScriptq2/taskRunner.ts(完整实现)q2/taskRunner.ts (the complete implementation)源项目From source
1export async function runTasks<T>(
2 tasks: Task<T>[],
3 limit: number,
4): Promise<SettledResult<T>[]> {
5 const results: SettledResult<T>[] = new Array(tasks.length);
6
7 let nextIndex = 0;
8
9 const worker = async () => {
10 while (nextIndex < tasks.length) {
11 const i = nextIndex;
12 nextIndex++;
13
14 try {
15 const value = await tasks[i]();
16 results[i] = { status: "fulfilled", value };
17 } catch (reason) {
18 results[i] = { status: "rejected", reason };
19 }
20 }
21 };
22 if (tasks.length === 0) return [];
23
24 const workerCount = Math.min(limit, tasks.length);
25 const workers: Promise<void>[] = [];
26 for (let w = 0; w < workerCount; w++) {
27 workers.push(worker());
28 }
29 await Promise.all(workers);
30 return results;
31}
Source: react-notes-app/q2/taskRunner.ts
§06

验证:读懂这段输出Checking your work: how to read this output

本机实测的完整输出。三条验收标准逐条对照:

  • 并发不超 2 —— 盯住 running now 那一列, 它在 1 和 2 之间来回,从来没到 3。✓
  • 顺序与输入一致 ——#1 是 task 1 的结果,尽管它耗时 300ms、 是第二个完成的。✓
  • 失败不连累别人 —— task 3 FAIL 之后,5、6 照常启动并完成,#3rejected,其余是 fulfilled。✓

另外注意 task 3 FAIL (running now: 1) 这一行: 失败也让槽位空了出来,紧接着 task 5 就启动了。失败和成功对调度器是一样的 —— 都只是「一个槽位空了」。

The full output as measured on this machine. Check the three criteria one by one:

  • Concurrency never above 2 — follow the running now column; it bounces between 1 and 2 and never reaches 3. ✓
  • Order matches the input#1 is task 1’s result even though it took 300ms and finished second. ✓
  • A failure does not drag the others down — after task 3 FAIL, 5 and 6 start and finish as usual,#3 is rejected and the rest are fulfilled. ✓

Also look at the line task 3 FAIL (running now: 1): a failure frees a slot too, and task 5 starts right after it. Failure and success look the same to the scheduler — both are just “a slot opened up”.

Text本机实测输出The real output from running it here源项目From source
1$ npm run q2
2
3task 1 START (running now: 1)
4task 2 START (running now: 2) ← 到上限,3 号排队
5task 2 DONE (running now: 1)
6task 3 START (running now: 2) ← 有槽位立刻补
7task 1 DONE (running now: 1)
8task 4 START (running now: 2)
9task 3 FAIL (running now: 1) ← 失败也只是空出槽位
10task 5 START (running now: 2)
11task 4 DONE (running now: 1)
12task 6 START (running now: 2)
13task 5 DONE (running now: 1)
14task 6 DONE (running now: 0)
15
16=== FINAL RESULTS (must be in original order) ===
17#1 { status: 'fulfilled', value: 'result of task 1' }
18#2 { status: 'fulfilled', value: 'result of task 2' }
19#3 {
20 status: 'rejected',
21 reason: Error: task 3 failed
22 at Timeout._onTimeout (.../q2/demo.ts:18:18)
23}
24#4 { status: 'fulfilled', value: 'result of task 4' }
25#5 { status: 'fulfilled', value: 'result of task 5' }
26#6 { status: 'fulfilled', value: 'result of task 6' }
1$ npm run q2
2
3task 1 START (running now: 1)
4task 2 START (running now: 2) ← at the limit, task 3 waits
5task 2 DONE (running now: 1)
6task 3 START (running now: 2) ← a slot opened, filled at once
7task 1 DONE (running now: 1)
8task 4 START (running now: 2)
9task 3 FAIL (running now: 1) ← a failure frees a slot too
10task 5 START (running now: 2)
11task 4 DONE (running now: 1)
12task 6 START (running now: 2)
13task 5 DONE (running now: 1)
14task 6 DONE (running now: 0)
15
16=== FINAL RESULTS (must be in original order) ===
17#1 { status: 'fulfilled', value: 'result of task 1' }
18#2 { status: 'fulfilled', value: 'result of task 2' }
19#3 {
20 status: 'rejected',
21 reason: Error: task 3 failed
22 at Timeout._onTimeout (.../q2/demo.ts:18:18)
23}
24#4 { status: 'fulfilled', value: 'result of task 4' }
25#5 { status: 'fulfilled', value: 'result of task 5' }
26#6 { status: 'fulfilled', value: 'result of task 6' }
Source: react-notes-app
练习Practice

动手做Get your hands on it

填空只是过渡。真正掌握的标准,是在没有答案的时候从头写出来 —— 所以做完 L2 之后一定要往 L3、L4 走。Filling blanks is a stepping stone. The real bar is writing it from nothing, so once L2 is comfortable, push on to L3 and L4.

L2填空Fill the blanks补全 worker pool 的五个关键位置Fill in the five key spots of the worker pool

五个空。第 2 个和第 4 个是最容易写错的 —— 一个关系到「顺序」,一个关系到「任务到底有没有被启动」。

Five blanks. Numbers 2 and 4 are the ones most often written wrong: one decides the order, the other decides whether the task was started at all.

TSq2/taskRunner.ts5 个空5 blanks
1export async function runTasks<T>(
2 tasks: Task<T>[],
3 limit: number,
4): Promise<SettledResult<T>[]> {
5 const results: SettledResult<T>[] = new Array(tasks.length);
6 let nextIndex = 0;
7
8 const worker = async () => {
9 while (nextIndex tasks.length) {
10 const i = nextIndex;
11 nextIndex++;
12
13 try {
14 const value = await ;
15 results[] = { status: "fulfilled", value };
16 } catch (reason) {
17 results[i] = { status: "", reason };
18 }
19 }
20 };
21
22 const workerCount = Math.(limit, tasks.length);
23 const workers: Promise<void>[] = [];
24 for (let w = 0; w < workerCount; w++) {
25 workers.push(worker());
26 }
27 await Promise.all(workers);
28 return results;
29}
把 5 个空都填上才能检查(还差 5 个)Fill all 5 blanks to check (5 to go)
L3写整块Write a block从签名开始,自己写出整个 runTasksStart from the signature and write all of runTasks yourself

只给签名。这是 Q2 的完整答案,写对了这道题就通了。 写完可以在本机 npm run q2 验证。

You get only the signature. This is the complete answer to Q2: get it right and the question is done. When you finish you can check it here with npm run q2.

要求Requirements
  • 同一时刻最多 limit 个任务在运行At most limit tasks are running at the same time
  • 某个任务结束后,立刻启动下一个(不是等一批都结束)As soon as one task finishes, start the next one (do not wait for a whole batch)
  • 任何任务失败都不能让整体抛错No failing task may make the whole call throw
  • 返回数组的顺序必须与 tasks 一致The order of the returned array must match tasks
  • 成功写 { status: "fulfilled", value },失败写 { status: "rejected", reason }On success write { status: "fulfilled", value }; on failure write { status: "rejected", reason }
TypeScriptq2/taskRunner.ts
This check is textual: it looks for the right constructs, it does not run your code
提示Hints共 4 级,已看 0 级4 levels, 0 opened
先自己想两分钟。想不出来再点右上角 —— 提示是一级一级放的,不会一次给完。Think for two minutes first. Then use the button above — hints come one level at a time, never all at once.

看答案之前,先确认你已经自己动手写过一遍。看懂别人的答案和自己写出来,是两种能力。Before you open this, make sure you have written it yourself once. Following someone else's answer and producing your own are two different skills.

L2Debug LabDebug LabDebug Lab · 一行 START 都没打印Debug Lab · not one START line prints

npm run q2,没有报错,但一行task N START 都没有,直接就出结果了 —— 而且结果里的 value 长得很奇怪。

You run npm run q2. Nothing reports an error, but not one task N START line appears; the results come out right away. And the value in each result looks strange.

第 1 步 · 先看现象和代码,别急着改Step 1 · read the symptom and the code before you touch anything
$ npm run q2 === FINAL RESULTS (must be in original order) === #1 { status: 'fulfilled', value: [Function (anonymous)] } #2 { status: 'fulfilled', value: [Function (anonymous)] } #3 { status: 'fulfilled', value: [Function (anonymous)] } #4 { status: 'fulfilled', value: [Function (anonymous)] } #5 { status: 'fulfilled', value: [Function (anonymous)] } #6 { status: 'fulfilled', value: [Function (anonymous)] } # 注意: # - 一行 "task N START" 都没有 # - 应该 reject 的 task 3 也变成了 fulfilled # - value 是函数,不是 "result of task N"$ npm run q2 === FINAL RESULTS (must be in original order) === #1 { status: 'fulfilled', value: [Function (anonymous)] } #2 { status: 'fulfilled', value: [Function (anonymous)] } #3 { status: 'fulfilled', value: [Function (anonymous)] } #4 { status: 'fulfilled', value: [Function (anonymous)] } #5 { status: 'fulfilled', value: [Function (anonymous)] } #6 { status: 'fulfilled', value: [Function (anonymous)] } # Note: # - not one "task N START" line was printed # - task 3, which should reject, came back fulfilled # - value is a function, not "result of task N"
TypeScript有问题的 workerThe worker with the problem示意Illustrative
1const worker = async () => {
2 while (nextIndex < tasks.length) {
3 const i = nextIndex;
4 nextIndex++;
5
6 try {
7 const value = await tasks[i];
8 results[i] = { status: "fulfilled", value };
9 } catch (reason) {
10 results[i] = { status: "rejected", reason };
11 }
12 }
13};
第 2 步 · 这是什么类型的错误Step 2 · what kind of error is this
错例Wrong

初学者常见的几种写法错误Mistakes beginners actually make

下面每一段都是「能编译、但结果不对」或者「一跑就炸」的真实写法。先自己看出问题在哪,再看解释。Every snippet below either compiles and gives the wrong answer, or blows up on the first run. Spot the problem yourself before reading the explanation.

TypeScript示意Illustrative
1// ✗ 用 push 收集结果 —— 顺序按完成时间排,不是按输入顺序
2try {
3 const value = await tasks[i]();
4 results.push({ status: "fulfilled", value });
5} catch (reason) {
6 results.push({ status: "rejected", reason });
7}
1// ✗ collecting results with push — the order follows finish time, not input order
2try {
3 const value = await tasks[i]();
4 results.push({ status: "fulfilled", value });
5} catch (reason) {
6 results.push({ status: "rejected", reason });
7}
push完成时间追加。 task 2(100ms)会排在 task 1(300ms)前面, 于是 #1 变成了 task 2 的结果。 违反「IN THE SAME ORDER as tasks」。
正解是预分配数组 + 按原始下标 results[i] 写回。
push appends in finish order. Task 2 (100ms) lands before task 1 (300ms), so #1 holds the result of task 2. That breaks the requirement to return results IN THE SAME ORDER as tasks.
The right answer: create the array up front and write each result to its original index, results[i].
TypeScript示意Illustrative
1// ✗ catch 之后 return,一个失败就停掉整个 worker
2try {
3 const value = await tasks[i]();
4 results[i] = { status: "fulfilled", value };
5} catch (reason) {
6 results[i] = { status: "rejected", reason };
7 return; // ← 这个 worker 死了
8}
1// ✗ returning after catch stops the whole worker on one failure
2try {
3 const value = await tasks[i]();
4 results[i] = { status: "fulfilled", value };
5} catch (reason) {
6 results[i] = { status: "rejected", reason };
7 return; // ← this worker is gone
8}
task 3 失败后,那个 worker 直接退出, 剩下的任务只能靠另一个 worker 慢慢做 —— 并发实际降到 1,而且如果两个 worker 都遇到失败, 后面的任务永远不会被执行,results 里留下 undefined 的洞。
catch 里只记录,不中断循环。
After task 3 fails, that worker exits. The remaining tasks are left to the other worker alone, so the real concurrency drops to 1. And if both workers hit a failure, the later tasks never run at all, which leaves undefined holes in results.
In the catch block, record what happened and let the loop continue.
TypeScript示意Illustrative
1// ✗ 每个 worker 各自维护一份游标
2const worker = async () => {
3 let nextIndex = 0; // ← let 写在了函数里面
4 while (nextIndex < tasks.length) { ... }
5};
1// ✗ every worker keeps its own cursor
2const worker = async () => {
3 let nextIndex = 0; // ← the let is inside the function
4 while (nextIndex < tasks.length) { ... }
5};
游标写在 worker 内部,每个 worker 就有自己的一份, 于是每个 worker 都从 0 开始把所有任务做一遍 —— limit=2 时每个任务被执行两次,并发数冲到 2 倍。
游标必须在 worker 外面声明, 靠闭包被所有 worker 共享。
With the cursor declared inside the worker, every worker gets its own copy, so every worker starts at 0 and runs all the tasks. With limit=2 each task runs twice, and the number running at once is double what it should be.
Declare the cursor outside the worker, so the closure shares one cursor between all workers.
迁移Transfer

换一道题也能用Works on other problems too

考试不会原题重考。真正能带走的是「看到这种信号 → 伸手去拿这个解法」。The exam will not reuse the same question. What you take away is the reflex: see this signal, reach for that solution.

「限制并发数」「连接池」「批量上传限速」Wording like: limit concurrency, a connection pool, rate-limited bulk upload
worker pool:共享游标 + limit 个 workerA worker pool: one shared cursor plus limit workers
「结果顺序必须与输入一致」The wording: results must be in the same order as the input
预分配数组 + results[i] 写回,别用 pushCreate the array up front and write results[i]; do not use push
「失败也要继续」The wording: keep going even when one fails
try/catch 在循环体内,catch 里不 returnPut try/catch inside the loop body, and do not return from the catch
结果里出现 [Function] 或 Promise {}A result prints as [Function] or as Promise {}
括号写少了或写多了You left out a pair of call parentheses, or added an extra pair
任务被重复执行A task runs more than once
游标是不是被声明在了 worker 内部Check whether the cursor was declared inside the worker
这节的要点What to take away
  1. worker pool 四个零件:预分配结果数组、共享游标、循环抢活的 worker、limit 个 worker + Promise.all。The four parts of a worker pool: a result array created up front, one shared cursor, a worker that loops and takes the next job, and limit workers run with Promise.all.
  2. 并发上限来自「worker 的个数」,顺序来自「按原始下标写回」—— 两件难事都不需要额外代码。The concurrency limit comes from how many workers you start. The order comes from writing each result to its original index. Neither needs extra code.
  3. JavaScript 单线程,游标那两行之间没有 await,所以不需要加锁。JavaScript runs on one thread, and there is no await between the two cursor lines, so no lock is needed.
  4. await tasks[i]() 的括号是关键;少了它任务根本不会被执行,而且不报错。The parentheses in await tasks[i]() matter. Without them the task never runs, and nothing reports an error.
  5. catch 里只记录不中断,这才叫「NEVER throws」。The catch block records and keeps going. That is what NEVER throws means.

接下来What next

  1. 把这一节的练习做掉Do this lesson’s exercises3 个,就在这一页上面 —— 别攒着最后一起做3 of them, further up this page — do not save them for later
    回到练习 ↑Back up to them ↑
  2. 接着看下一节Continue to the next lesson变式一 · Todo ListVariation 1 · Todo List
    下一节Next lesson
读完并且做过上面的练习了吗?Read it and worked through the exercises above?
上一节:Previous: 读题:三条要求,每一条都在指定一种写法Reading the question: three requirements, and each one decides how you write it