原创

Phaser 有人退出后为什么仍未推进:数清当前轮的参与资格

两个工作者都已经报告“这一轮结束”,协调线程却仍然卡在阶段零。先数注册参与者:如果协调线程自己也是一个 party,它没有到达,阶段本来就不该推进。Phaser 记的是注册与到达计数,不是“线程池里有几个线程”。

离开也不是把线程对象从某个集合里删除。arriveAndDeregister 同时表达当前阶段已经到达,并减少后续阶段的注册数量。留下来的参与者仍然必须完成自己的到达,剩余人数不会因为有人退出就自动归零。

下面让一名工作者只参加第一轮,另一名继续到第二轮。门闩固定观察时点,避免打印计数时线程已经进入下一阶段。Java 17 保存为 DynamicParties.java 后执行 java DynamicParties.java:

import java.util.concurrent.*;

public class DynamicParties {
    static void await(CountDownLatch latch) throws InterruptedException {
        if (!latch.await(3, TimeUnit.SECONDS)) throw new IllegalStateException("coordination timeout");
    }
    public static void main(String[] args) throws Exception {
        Phaser phaser = new Phaser(1); // Coordinator is one registered party.
        phaser.bulkRegister(2);
        ExecutorService workers = Executors.newFixedThreadPool(2);
        CountDownLatch arrived = new CountDownLatch(2);
        CountDownLatch phaseOne = new CountDownLatch(1);
        CountDownLatch mayLeave = new CountDownLatch(1);
        try {
            Future<Integer> continuing = workers.submit(() -> {
                int current = phaser.arrive();
                arrived.countDown();
                int next = phaser.awaitAdvanceInterruptibly(current, 3, TimeUnit.SECONDS);
                phaseOne.countDown();
                await(mayLeave);
                phaser.arriveAndDeregister();
                return next;
            });
            Future<Integer> leaving = workers.submit(() -> {
                int current = phaser.arriveAndDeregister();
                arrived.countDown();
                return current;
            });
            await(arrived);
            System.out.println("before coordinator: phase=" + phaser.getPhase()
                    + ", parties=" + phaser.getRegisteredParties()
                    + ", unarrived=" + phaser.getUnarrivedParties());
            int advanced = phaser.arriveAndAwaitAdvance();
            await(phaseOne);
            System.out.println("after coordinator: phase=" + advanced
                    + ", parties=" + phaser.getRegisteredParties());
            mayLeave.countDown();
            System.out.println("continuing saw=" + continuing.get(3, TimeUnit.SECONDS));
            System.out.println("leaving arrived in=" + leaving.get(3, TimeUnit.SECONDS));
            phaser.arriveAndDeregister();
            System.out.println("terminated=" + phaser.isTerminated());
            System.out.println("register after termination is negative=" + (phaser.register() < 0));
        } finally {
            mayLeave.countDown();
            phaser.forceTermination();
            workers.shutdownNow();
            if (!workers.awaitTermination(3, TimeUnit.SECONDS)) throw new IllegalStateException("workers active");
        }
    }
}
before coordinator: phase=0, parties=2, unarrived=1
after coordinator: phase=1, parties=2
continuing saw=1
leaving arrived in=0
terminated=true
register after termination is negative=true

开始有三名注册参与者:协调者和两个工作者。离开的工作者完成当前到达并注销,另一名工作者已经到达并等待,所以观察到注册数二、未到达数一。缺少的正是协调者,不是一个神秘丢失的工作线程。

协调者到达后,剩下的两名参与者进入阶段一。继续工作者获准退出以后,协调者最后也注销,注册数变成零,默认 Phaser 随之终止。终止后 register 返回负值,不能靠再注册一名工作者把这份对象复活成新的一轮协调器。

arrive 与 await 是两个动作

arrive() 只登记当前阶段的到达,不等待别人;返回的是到达时的阶段号。awaitAdvance 根据这个阶段号等待变化,却不会替调用者补上一次 arrive。若只 await、不 arrive,可能就是让其他人一直等自己。

程序中 continuing 先 arrive,再使用可中断、带上限的等待。协调者用 arriveAndAwaitAdvance 合并两步,因为在它调用之前,两个工作者的到达已经由独立门闩确认。通用业务若无法确认对方一定到达,就应另行设计超时或终止,而不是无限等待。

还要区分等待上限与参与者注销。某个线程等待超时或被中断,不会自动让它从 Phaser 中离开;其他参与者可能继续等待它后续阶段的到达。异常路径应由协议决定注销还是终止整个计算,不能只捕获异常并让线程悄悄返回。

Phaser 不记录每个人的身份

注册数量和实际任务必须由调用方保持一致。一个线程重复 arrive,Phaser 不会用线程 ID 判断“你已经签过到”,而是可能破坏当前轮次计数;一个已经注销的任务继续随意 arrive,也不符合它的使用合同。

因此不要让业务方法在多个 finally 里无条件 arriveAndDeregister。如果某条路径已经完成本轮到达,清理时再注销可能表达了另一轮动作。可以让每个角色集中管理自己的注册寿命,明确什么时候持有一份参与资格、什么时候已经交回。

动态注册同样不是“任务提交了就自动加入”。注册与实际启动之间若失败,需要补偿那份资格;反过来,先把任务排进执行器,再让它自己决定注册,协调者可能在它还没运行之前就推进阶段。程序选择预先 bulkRegister,阶段零便知道自己究竟等谁。

计数适合观察,不适合当并发裁判

getRegisteredParties、getUnarrivedParties 和 getPhase 在系统运行时会变化。读取三个值不自动构成同一个原子快照;本例使用门闩把工作者暂停,才让打印具有可以重算的含义。真实监控可以展示趋势,但不应通过一次“未到达数等于一”就决定谁可以替别人签退。

forceTermination 会让等待阶段推进的线程看到终止状态,却不等于中断所有工作线程。代码最后仍然关闭执行器,并释放独立门闩。任务若还在等待网络、文件或别的锁,需要相应取消机制;一个同步器结束,不能代替所有资源的寿命管理。

如果每轮参与者固定,可以选用更直接的屏障工具;若只是等待一批任务结束,一次性门闩也许更容易说明。Phaser 的价值在于多轮与参与者增减,但这也要求每条退出路径都能解释它交回的是哪一份资格。先画清“本轮到达”和“以后不再参加”两个动作,往往就能找出阶段为什么没有推进。

正文到此结束
评论插件初始化中...
Loading...
本文目录