2.2 异步 Publisher 与背压信号

实现带背压的异步 Publisher——AsyncRangePublisher:用饱和加法累加 request、用 CAS 扣减额度、用 done 标志保证终止信号只发一次,并解释为什么必须用单线程 Executor 保证信号串行。给出完整实现、取消与空范围测试,以及三个容易写错的并发细节。

从同步到异步:什么变了

上一节的 SyncSinglePublisher 在调用线程同步发射。真实数据源通常异步:网络响应、数据库驱动回调、消息队列拉取、定时器触发。

异步化带来三个新问题:

  1. request(n) 在消费者线程调用,onNext 在生产线程调用——两者可能并发,额度记账必须是原子的。
  2. cancel() 可能从任意线程到达——必须原子地停止生产,且不能阻塞调用者。
  3. 信号必须串行(§1.3)——即使内部有多个生产线程,到达同一个 Subscriber 的 onNext 不能重叠。

AsyncRangePublisher 的设计

AsyncRangePublisher 发射整数区间 [from, to),发射逻辑提交到 Executor。它演示四个机制:

  • request 额度累加(饱和,防溢出)
  • 额度扣减与发射的原子配合
  • 单线程 Executor 保证信号串行
  • done 标志保证 onComplete/onError 只发一次

完整实现

package com.plume.eventd.core;

import java.util.concurrent.Executor;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicLong;
import org.reactivestreams.Publisher;
import org.reactivestreams.Subscriber;
import org.reactivestreams.Subscription;

/**
 * 异步范围 Publisher:在 Executor 上按 request 额度发射 [from, to)。
 *
 * <p>调用方必须传入单线程 Executor(或串行化的 Executor),
 * 否则 §1.3 的信号串行无法保证。
 */
public final class AsyncRangePublisher implements Publisher<Integer> {

    private final int from;
    private final int to;
    private final Executor executor;

    public AsyncRangePublisher(int from, int to, Executor executor) {
        if (from > to) throw new IllegalArgumentException("from > to");
        this.from = from;
        this.to = to;
        this.executor = executor;
    }

    @Override
    public void subscribe(Subscriber<? super Integer> subscriber) {
        if (subscriber == null) throw new NullPointerException("subscriber");
        subscriber.onSubscribe(new RangeSubscription(subscriber, from, to, executor));
    }

    private static final class RangeSubscription implements Subscription {

        private final Subscriber<? super Integer> subscriber;
        private final int to;
        private final Executor executor;

        private int current;
        private final AtomicLong requested = new AtomicLong();
        private final AtomicBoolean cancelled = new AtomicBoolean();
        private final AtomicBoolean done = new AtomicBoolean();
        private final AtomicBoolean emitting = new AtomicBoolean();
        private volatile Throwable error;

        RangeSubscription(Subscriber<? super Integer> subscriber,
                          int from, int to, Executor executor) {
            this.subscriber = subscriber;
            this.current = from;
            this.to = to;
            this.executor = executor;
        }

        @Override
        public void request(long n) {
            if (cancelled.get() || done.get()) {
                return;                       // §3.6:cancel 后 request 是 no-op
            }
            if (n <= 0) {
                // 不能在这里直接调 onError:request 可能来自另一个线程,
                // 会与发射线程的 onNext 并发。记下错误,交给 emitLoop 串行交付
                error = new IllegalArgumentException(
                        "§3.9: non-positive request: " + n);
                tryEmit();
                return;
            }
            if (!addDemand(n)) {
                return;
            }
            tryEmit();
        }

        @Override
        public void cancel() {
            // §3.5:快速返回,不同步做重计算;§3.7:重复 cancel 是 no-op
            cancelled.set(true);
        }

        /** 饱和加法:任意次 request 累加都不溢出(§3.17)。 */
        private boolean addDemand(long n) {
            for (;;) {
                long r = requested.get();
                if (r == Long.MAX_VALUE) return true;
                long updated = r + n;
                if (updated < 0) updated = Long.MAX_VALUE;
                if (requested.compareAndSet(r, updated)) return true;
            }
        }

        private void tryEmit() {
            // 同一时刻只允许一个发射任务;否则并发 request 会重复调度
            if (emitting.compareAndSet(false, true)) {
                executor.execute(this::emitLoop);
            }
        }

        private void emitLoop() {
            try {
                for (;;) {
                    if (cancelled.get() || done.get()) return;

                    Throwable terminal = error;
                    if (terminal != null) {
                        signalError(terminal);   // 与 onNext 在同一线程串行交付
                        return;
                    }

                    long req = requested.get();
                    if (req == 0) return;     // 额度用完,等下一次 request

                    long available = (long) to - current;
                    if (available <= 0) {
                        complete();            // 元素耗尽
                        return;
                    }

                    int emitCount = (int) Math.min(req, available);

                    // 先扣额度再发射:即使中途 cancel,也只少发、不会超发
                    if (!requested.compareAndSet(req, req - emitCount)) {
                        continue;              // 有并发 request,用新额度重试
                    }

                    for (int i = 0; i < emitCount; i++) {
                        if (cancelled.get()) return;
                        terminal = error;
                        if (terminal != null) {
                            signalError(terminal);
                            return;
                        }
                        subscriber.onNext(current++);
                    }
                }
            } catch (Throwable t) {
                signalError(t);                // 下游抛异常:转成 onError
            } finally {
                emitting.set(false);
                // 退出瞬间可能有新 request 或错误到达,补一次调度
                if ((requested.get() > 0 || error != null)
                        && !cancelled.get() && !done.get()) {
                    tryEmit();
                }
            }
        }

        private void complete() {
            if (done.compareAndSet(false, true) && !cancelled.get()) {
                subscriber.onComplete();       // §1.7:终止信号只发一次
            }
        }

        private void signalError(Throwable t) {
            if (done.compareAndSet(false, true)) {
                subscriber.onError(t);
            }
        }
    }
}

四个关键设计决策

1. 额度累加必须饱和

long updated = r + n;
if (updated < 0) updated = Long.MAX_VALUE;

如果直接 requested.addAndGet(n),累计需求超过 Long.MAX_VALUE 后会溢出成负数。之后 req == 0 判断失效、Math.min 得到负的发射数量,发布者可能永久挂起或直接崩溃。§3.17 允许 Publisher 把 Long.MAX_VALUE 视为无界,所以溢出时饱和到上限是正确的。

2. 先扣额度,再发射

if (!requested.compareAndSet(req, req - emitCount)) continue;
for (int i = 0; i < emitCount; i++) subscriber.onNext(current++);

反过来「先发再扣」会在并发 request 下重复计算额度:两个线程都读到 req=10,各自发 10 个,合计 20,违反 §1.1。先扣再发保证「已扣的额度 ≥ 已发的元素」,最坏情况是中途 cancel 导致少发——§1.2 明确允许少发后终止。

CAS 失败时用 continue 重新读取额度,而不是放弃。并发 request 增加了额度,新的循环会用更大的 req 继续。

3. emitting 只负责调度互斥

if (emitting.compareAndSet(false, true)) {
    executor.execute(this::emitLoop);
}

request 可能被连续调用多次:第一次启动发射任务,后面几次只加额度,不应再提交任务。emitting 就是这把锁:任务运行期间为 true,finally 中置回 false,并检查是否还有未消费的额度。

注意它不保证信号串行。信号串行来自 Executor:只有单线程(或所有任务都串行执行的)Executor 才能保证两个 emitLoop 不重叠。构造器接受任意 Executor 是妥协——如果调用方传线程池,emitting 仍能让同一时刻只有一个任务在发信号,但 current++ 和 requested 的可见性依赖 Executor 的提交语义;更稳妥的做法是在类型上就要求 ScheduledExecutorService 单线程包装。生产代码里 Reactor 用 Sinks 内部的 WIP 计数器解决同一问题,不依赖调用方。

4. done 防止终止信号重入

没有 done 时会出现一个隐蔽死循环:元素耗尽后 onComplete 被调用,但 requested 仍大于 0;finally 看到「还有额度且未取消」,再次调度 emitLoop;新任务发现 available <= 0,又一次 onComplete……只要 Subscriber 请求过超过元素总数的额度,这个循环就会持续提交任务、持续重复终止信号,违反 §1.7,还会把 Executor 队列打满。

done.compareAndSet(false, true) 让 complete() 和 signalError() 互斥且只执行一次,finally 也检查 done,循环终止。

运行验证

基本用法

import java.util.concurrent.Executors;
import java.util.concurrent.ExecutorService;
import org.reactivestreams.Subscriber;
import org.reactivestreams.Subscription;
import com.plume.eventd.core.AsyncRangePublisher;

public class AsyncRangeTest {
    public static void main(String[] args) throws Exception {
        ExecutorService executor = Executors.newSingleThreadExecutor();
        new AsyncRangePublisher(1, 6, executor).subscribe(new Subscriber<>() {
            private Subscription subscription;
            private int count;

            @Override
            public void onSubscribe(Subscription s) {
                this.subscription = s;
                System.out.println("onSubscribe");
                s.request(2);
            }

            @Override
            public void onNext(Integer i) {
                System.out.println("onNext: " + i);
                if (++count == 2) {
                    subscription.request(3);
                }
            }

            @Override public void onError(Throwable t) { t.printStackTrace(); }
            @Override public void onComplete() { System.out.println("onComplete"); }
        });

        Thread.sleep(200);       // 演示用;生产代码不要用 sleep 等异步结果
        executor.shutdown();
    }
}

输出:

onSubscribe
onNext: 1
onNext: 2
onNext: 3
onNext: 4
onNext: 5
onComplete

前两个来自 request(2),后三个来自 onNext(2) 里补的 request(3)。所有 onNext 都跑在 Executor 线程上。

取消后停止

@Override
public void onNext(Integer i) {
    System.out.println("onNext: " + i);
    if (i == 3) subscription.cancel();
}

输出 onNext: 1 到 onNext: 3,之后没有 onComplete。cancel 置位后,发射循环在下一个元素前检查并退出。

空范围

new AsyncRangePublisher(5, 5, executor):第一次 request 后 available = 0,直接 onComplete。输出只有 onComplete。

三个并发细节

细节一:onComplete 可能早于 request 返回

同步 Executor(如 Runnable::run)下,request(10) 会在返回前完成全部发射和 onComplete。异步 Executor 下,request 返回时可能一个元素都还没发。Subscriber 不能假设「request 返回即已收到」——这是 2.9/2.10 要求「无论有无 request 都要准备接收终止信号」的现实原因。

细节二:cancel 之后仍可能收到 onNext

cancel() 只是置位,发射循环正在执行的 onNext 不会被打断;如果循环已经进入 for 且通过了 cancelled 检查,当前这个元素仍会送达。这符合 §2.8:Subscriber 必须准备好接收 cancel 之后、已经请求过的迟到元素。

细节三:下游抛异常必须转成 onError

catch (Throwable t) {
    signalError(t);
}

§2.13 要求 Subscriber 的四个方法正常返回;如果它违反了(例如 onNext 里抛 RuntimeException),Publisher 不能把异常吞掉,也不能让发射线程带着异常退出。正确做法是终止订阅并(如果尚未终止)调用 onError。这里 signalError 用 done 保证不会在 onComplete 之后再发 onError。

小结

机制实现对应规则
额度累加饱和 CAS§3.8、§3.17
先扣后发compareAndSet§1.1
调度互斥emitting避免重复任务
终止唯一done§1.7
取消cancelled§3.12

下一节转向消费端:实现 LoggingSubscriber,看 Subscriber 一侧的规则如何落到代码。

阅读导航

上一篇:2.1 同步 Publisher 最小实现

下一篇:2.3 Subscriber 实现与信号契约

继续阅读

探索更多技术文章

浏览归档,发现更多关于系统设计、工具链和工程实践的内容。

全部文章 返回首页

「java」更多文章

  1. 15.3 全书总结与学习路径回顾
  2. 15.2 响应式流处理的演进趋势
  3. 15.1 引擎选型指南