开始前,会这些就够了
- 了解生产者把任务放入队列、消费者取出任务的流程。
- 能读懂 Python try
/finally和线程启动。 - Python 3.10+,只使用标准库。
- 需要补基础时:先补线程与共享状态
单消费者版本往队列里放一个特殊值,工作线程读到后退出。一改成三个消费者,所有任务似乎都处理完了,主程序却结束不了:停止标记也是普通队列中的一项,不会自动广播给每个线程。
我们启动三个真实工作线程,先只放一个停止标记,并等待队列结账。此时有两个线程仍活着。再补两个标记,并逐个等待线程退出,才能得到完整的关闭结果。
把例子带到自己的电脑上
下载完整练习包
需要 Python 3.10 或更新版本,无第三方依赖;制作时实测 Windows / Python 3.14.0。另附可选 Java 标准库版本,需 JDK 17 或更新版本;实测 JDK 17 与 21。下方命令运行基础 Python 标准库示例;可选对照的环境和命令见对应 README。下载 ZIP 后选择“全部提取”,进入能看到 README.md 的文件夹。
下载 ZIP · 14 个文件包里有什么
- demo.py:完整可运行示例
- README.md:运行说明与独立练习
- observed.stdout.txt:本次实际输出
- verification.json:源码和产物验证记录
- SHA256SUMS.txt:内容摘要
- java/:Java 源码、中文 README、JDK 17/21 输出和验证记录
- supplement-verification.json:附加实现的独立复跑记录
在这个文件夹打开 PowerShell,逐行运行
python --version
python -B demo.py解压到独立练习文件夹,在含 demo.py 的目录打开终端运行。Windows本机3个真实工作线程,单/三停止标记、任务成功失败记录、两类join与6项检查;未实现动态消费者和多生产者生产框架 压缩包的 java/ 子目录另附 Java 标准库版本、中文 README、两份实测输出和验证记录;进入 java/ 后按 README 编译运行。
找不到命令或文件时,先按 README 检查环境和所在目录。文中标出的阅读时间只估算文章篇幅,安装环境和动手练习可按自己的节奏进行。
队列取走一项,其他消费者就不会再取到它
本例的 stop 是一个专用对象,消费者用 is 判断是否为结束标记。它与正常任务放在同一个队列里,遵守相同的取出规则:一次 get 把这一项交给一个消费者,不会把对象复制给所有消费者。
读到 stop 的线程退出,另外两个线程仍循环调用 get,等待新任务。队列空了不等于以后永远没有任务,因此它们没有理由凭一次空结果自行退出;这正是生产者与消费者需要停止协议的原因。
这里消费者数量固定为三,且主线程是唯一生产者。先放完三项业务任务,再开始发送停止标记。这个前提让“每个消费者一个标记”足够清楚;有多个仍在生产的来源或动态消费者时,还要另外协调什么时候可以宣告输入结束。
task_done 是入队项的结账,不是成功印章
每次 put 会增加未完成项的计数,消费者处理后调用 task_done 使计数减少。queue.join 等这个计数归零,所以它关注入队项有没有得到配对的结账调用,而不是有多少个线程已经退出。
脚本把 task_done 放进取出一项后的 finally,因此成功任务、记录过失败的任务和停止标记各结账一次。停止标记虽然不执行业务计算,也确实经历了一次 put/get;漏掉它的结账会让 queue.join 等不到归零。
业务输入固定为 2、0、5,每项计算 10 // 输入值,也就是整数除法;成功列表保存(输入值,计算结果),失败列表只保存输入值。因此任务零触发除零错误,另外两项得到 (2, 5) 和 (5, 2)。队列仍能结账完毕,因为失败已经被明确记录并结束处理;这不意味着任务零成功了,也不意味着队列会替你重试。
queue.join 与 Thread.join 回答不同问题
第一次 queue.join 返回后,脚本确认只记录到一个退出的消费者,并等待那个线程实际结束。此时再数 is_alive,得到两个。这组证据比只看“队列长度为零”更直接地显示了还没完成的线程生命周期。
随后给剩下的两个线程各放一个 stop,再等队列结账,最后对每个线程执行带保护时限的 join,并检查 is_alive 全部为假。Thread.join 的返回值本身不能说明是否等到退出,所以代码还要读线程状态。
你可以把这两个问题分别写在关闭清单中:已接受的队列项有没有处理结果;运行这些任务的工作线程有没有结束。把两项都叫“全部完成”,很容易漏掉其中一种等待。
实验保护让错误结果能够被看见并正常收尾
主线程通过 Barrier 等三个消费者都已启动,再投递任务;结果按任务值排序打印,不依赖哪个线程恰好抢到哪一项。因此线程调度顺序可以不同,而我们关注的业务记录与退出数量保持可比较。
queue.join 本身没有超时参数,示例用一个监视线程执行它,主线程只等待三秒;消费者的 get 也有短超时,用来检查异常清理事件。finally 会通知退出、结清尚未取走的演示项并回收监视线程,避免故意观察错误时留下等待者。
这些保护属于实验设施,不是完整生产队列框架。特别是异常清理时结清未执行项,只用于回收本次教学程序,不能把那些项计入业务成功。正常输出中的成功与失败来自此前独立记录的处理结果。
运行后同时看三项证据
保存为 demo.py 后执行 python -B demo.py。程序不联网、不读写业务文件,只创建三个工作线程及有限的队列等待监视线程。结果中应同时保留成功列表、失败列表和两个阶段的存活线程数。
本次 Python 3.14.0、Windows 实跑完成六项检查:一个标记后存活数为二,三个标记后为零;成功任务是二和五,失败任务是零。最后 all_threads_stopped=True 在 finally 清理完成后才打印。
展开收起代码 · 小实验
长行可在代码区域内左右滚动,按 Tab 可把焦点移入代码区。
"""One stop item is consumed once; queue.join is not Thread.join.
All waiting has a bounded escape path for this finite demonstration.
"""
from queue import Empty, Queue
from threading import Barrier, Event, Lock, Thread
def main():
jobs = Queue()
stop = object()
abort = Event()
ready = Barrier(4, timeout=3)
lock = Lock()
successes, failures, stopped = [], [], []
monitors = []
checks = 0
def check(condition, message):
nonlocal checks
if not condition:
raise AssertionError(message)
checks += 1
def worker(index):
ready.wait()
while not abort.is_set():
try:
item = jobs.get(timeout=0.1)
except Empty:
continue
try:
if item is stop:
with lock:
stopped.append(index)
return
try:
result = 10 // item
except ZeroDivisionError:
with lock:
failures.append(item)
else:
with lock:
successes.append((item, result))
finally:
# The stop item and a failed work item also need one task_done.
jobs.task_done()
def wait_for_queue():
monitor = Thread(target=jobs.join, name="queue-join-monitor")
monitors.append(monitor)
monitor.start()
monitor.join(timeout=3)
if monitor.is_alive():
raise AssertionError("queue.join did not finish within the guard")
workers = [Thread(target=worker, args=(i,), name=f"worker-{i}") for i in range(3)]
try:
for thread in workers:
thread.start()
ready.wait()
for item in (2, 0, 5):
jobs.put(item)
jobs.put(stop)
wait_for_queue()
with lock:
stopped_copy = list(stopped)
check(len(stopped_copy) == 1, "one stop item must be consumed by one worker")
workers[stopped_copy[0]].join(timeout=3)
alive_after_one = sum(thread.is_alive() for thread in workers)
check(alive_after_one == 2, "queue.join does not stop the other workers")
check(sorted(successes) == [(2, 5), (5, 2)], "successful work is recorded separately")
check(failures == [0], "task_done is not a business-success certificate")
print(f"after_one_stop: queue_join_returned=True; workers_alive={alive_after_one}")
print(f"work_successes={sorted(successes)}; work_failures={failures}")
for _ in range(alive_after_one):
jobs.put(stop)
wait_for_queue()
for thread in workers:
thread.join(timeout=3)
check(not any(thread.is_alive() for thread in workers), "all workers must exit")
check(sorted(stopped) == [0, 1, 2], "each worker consumes its own stop item")
print("after_three_stops: queue_join_returned=True; workers_alive=0")
finally:
abort.set()
ready.abort()
for thread in workers:
if thread.ident is not None:
thread.join(timeout=3)
# Release a join monitor even if an earlier assertion interrupted the run.
while True:
try:
jobs.get_nowait()
except Empty:
break
else:
jobs.task_done()
for monitor in monitors:
monitor.join(timeout=3)
if any(t.is_alive() for t in workers + monitors):
raise AssertionError("demonstration thread did not stop")
print(f"PASS checks={checks}; all_threads_stopped=True")
if __name__ == "__main__":
main()
怎样运行:在独立练习文件夹中执行 python -B demo.py。2026-10-17 已在 Windows、Python 3.14.0 运行此份源码并通过自带检查。Windows本机3个真实工作线程,单/三停止标记、任务成功失败记录、两类join与6项检查;未实现动态消费者和多生产者生产框架
运行后对照结果
after_one_stop: queue_join_returned=True; workers_alive=2
work_successes=[(2, 5), (5, 2)]; work_failures=[0]
after_three_stops: queue_join_returned=True; workers_alive=0
PASS checks=6; all_threads_stopped=True换成 Java:BlockingQueue 不自带任务销账
Java BlockingQueue 提供放入和取出任务的操作,但没有 Python Queue.task_done 或 Queue.join 这套计数接口。本例另外为固定的三项业务任务创建 CountDownLatch(3),每项处理结束后在 finally 调用 countDown;这本业务账属于示例本身,不是 BlockingQueue 自动维护的。
除零任务也会先记录失败、再减少业务计数,所以 await 返回仍不证明所有业务成功。停止对象只负责告诉消费者退出,没有加入这次三项业务的 latch。三个消费者中只发送一个停止对象,业务 latch 虽已归零,仍会有两个线程继续等待;再发送两个停止对象并分别 Thread.join,才核实三个工作者全部退出。
保存为 BlockingQueueStopDemo.java 后编译运行。Windows 的 JDK 17.0.20.1 与 21.0.12 都通过 8 项检查。CountDownLatch 的起始值固定,也不能重置;这个例子适合已知三项任务的批次,没有实现可无限追加任务的队列计数框架。
展开收起代码 · 小实验
长行可在代码区域内左右滚动,按 Tab 可把焦点移入代码区。
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.TreeMap;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
/** Three fixed consumers; completion accounting is separate from BlockingQueue. */
public final class BlockingQueueStopDemo {
private record Job(int value, boolean stop) {}
private static final Job STOP = new Job(0, true);
private static int checks;
private static void check(boolean condition, String message) {
if (!condition) throw new AssertionError(message);
checks++;
}
private static void await(CountDownLatch latch, String description) throws InterruptedException {
if (!latch.await(3, TimeUnit.SECONDS)) {
throw new AssertionError(description + " did not finish within the guard");
}
}
public static void main(String[] args) throws Exception {
BlockingQueue<Job> jobs = new LinkedBlockingQueue<>();
CountDownLatch ready = new CountDownLatch(3);
CountDownLatch businessDone = new CountDownLatch(3);
CountDownLatch firstStop = new CountDownLatch(1);
AtomicBoolean abort = new AtomicBoolean();
Map<Integer, Integer> successes = new ConcurrentHashMap<>();
ConcurrentLinkedQueue<Integer> failures = new ConcurrentLinkedQueue<>();
ConcurrentLinkedQueue<Integer> stopped = new ConcurrentLinkedQueue<>();
ConcurrentLinkedQueue<String> workerErrors = new ConcurrentLinkedQueue<>();
List<Thread> workers = new ArrayList<>();
for (int index = 0; index < 3; index++) {
final int id = index;
workers.add(new Thread(() -> {
ready.countDown();
try {
while (!abort.get()) {
Job job = jobs.take();
if (job == STOP) {
stopped.add(id);
firstStop.countDown();
return;
}
try {
successes.put(job.value(), 10 / job.value());
} catch (ArithmeticException expected) {
failures.add(job.value());
} finally {
// This explicit latch belongs to our finite batch.
businessDone.countDown();
}
}
} catch (InterruptedException interrupted) {
if (!abort.get()) workerErrors.add("unexpected worker interruption");
Thread.currentThread().interrupt();
} catch (RuntimeException error) {
workerErrors.add(error.toString());
}
}, "queue-worker-" + id));
}
try {
for (Thread worker : workers) worker.start();
await(ready, "worker startup");
for (int value : new int[] {2, 0, 5}) jobs.add(new Job(value, false));
jobs.add(STOP);
await(businessDone, "business batch");
await(firstStop, "first stop item");
check(stopped.size() == 1, "one stop item must stop exactly one consumer");
workers.get(stopped.element()).join(3000);
long aliveAfterOne = workers.stream().filter(Thread::isAlive).count();
check(aliveAfterOne == 2, "business completion does not stop the other workers");
check(businessDone.getCount() == 0 && jobs.isEmpty(),
"the finite batch is accounted for and no queue item remains");
check(successes.equals(Map.of(2, 5, 5, 2)), "successful work must be recorded");
check(List.copyOf(failures).equals(List.of(0)), "failed work must be recorded separately");
System.out.println("after_one_stop: business_latch=0; workers_alive=" + aliveAfterOne);
System.out.println("work_successes=" + new TreeMap<>(successes) + "; work_failures=" + failures);
for (int i = 0; i < aliveAfterOne; i++) jobs.add(STOP);
for (Thread worker : workers) worker.join(3000);
check(workers.stream().noneMatch(Thread::isAlive), "all consumers must exit");
check(stopped.stream().sorted().toList().equals(List.of(0, 1, 2)),
"each consumer must receive a stop item");
check(workerErrors.isEmpty(), "no unexpected worker error may be hidden");
System.out.println("after_three_stops: workers_alive=0; stopped_workers=[0, 1, 2]");
} finally {
abort.set(true);
for (Thread worker : workers) worker.interrupt();
for (Thread worker : workers) worker.join(3000);
if (workers.stream().anyMatch(Thread::isAlive)) {
throw new AssertionError("demonstration worker did not stop");
}
jobs.clear();
}
System.out.println("PASS checks=" + checks + "; all_threads_stopped=true");
}
}
怎样运行:javac -encoding UTF-8 --release 17 BlockingQueueStopDemo.java
java -cp . BlockingQueueStopDemo
运行后对照结果
after_one_stop: business_latch=0; workers_alive=2
work_successes={2=5, 5=2}; work_failures=[0]
after_three_stops: workers_alive=0; stopped_workers=[0, 1, 2]
PASS checks=8; all_threads_stopped=true有专用关闭 API 时,仍要核对它承诺的事
Python 3.13 起提供 Queue.shutdown,使用相应版本时可以进一步学习它的正常与立即关闭语义。本例为了兼容 Python 3.10,采用显式标记;不要把两套协议混用后仍假设计数行为完全相同。
Java BlockingQueue 接口本身没有统一的关闭操作,官方文档也提到特殊结束对象的做法。移植时不能照搬 Python 的 task_done,因为那是具体 API;可沿用的是将输入结束、处理结果与工作者退出分开检查的思路。
记住这三点
- 一个停止标记是一个队列项,通常只会被一个消费者取走。
task_done与queue.join跟踪项的处理结账,业务成功要单独记录。- 队列结账之后,还应等待并核对工作线程已经退出。
别把例子用过头
- 按单消费者写一个结束标记,再直接扩成多个消费者。
- 停止标记通过 return 离开,却漏掉
task_done。 - 看到
queue.join返回就宣称所有任务成功或所有线程退出。 - 生产者尚未结束时就发送标记,导致标记后还有无人处理的业务项。
自己试一小步
把示例改成四个消费者、两个生产者,怎样保证所有业务项先进入队列,再发出足够的结束标记?
- 先定义两个生产者怎样报告自己不再产生任务。
- 分别记录每一项任务的成功、失败或未完成状态。
- 只在约定的输入结束条件成立后安排消费者退出。
- 测试一个生产者提前失败的情况,并验证关闭流程不会把未完成项写成成功。
做到这些,就算完成
- 停止标记数量与实际需要停止的消费者对应。
- 每次 get 恰好配对一次
task_done,包括标记和失败项。 - 结束时独立核对业务记录、队列结账和线程存活状态。
查原始资料
想确认一个细节,先看这份资料对应的部分,不必从头读完所有文档。
- Python 3.14:Queue.task_done、join 与 shutdown (新标签页)↗
核对每个入队项的未完成计数、join 返回条件及新关闭 API 的版本边界。
- Python 3.14:Thread.join 与 is_alive (新标签页)↗
说明带超时 join 后还需检查线程是否存活。
- Java 21 BlockingQueue:结束对象与接口范围 (新标签页)↗
核对接口方法列表和不内置 close
/shutdown、可约定结束对象的说明。 - Java 21 CountDownLatch:固定起始计数 (新标签页)↗
核对
countDown、await、一次性不可重置的计数语义。 - Java 21 Thread.join (新标签页)↗
核对有时限地等待线程结束,并在返回后另查
isAlive。