13-6.【Combine】什么是“Backpressure”?Combine 如何处理?

4 阅读3分钟

在响应式编程中,Backpressure(背压) 是一个至关重要的概念。简单来说,它描述的是:当数据的产生速度(Publisher)超过了数据的处理速度(Subscriber)时,系统如何进行协调,防止内存溢出或程序崩溃。

想象一个漏斗:如果上方倒水的速度远快于下方细管流出的速度,漏斗就会溢出。Backpressure 就是一套机制,让下方的细管能告诉上方的壶:“慢点倒,我处理不过来了。”


1. Combine 处理 Backpressure 的核心:拉取模型(Pull Model)

与许多早期的响应式框架(如传统的 Observable “推”模型)不同,Combine 建立在 基于需求的拉取模型(Demand-based Pull Model) 之上。

核心机制:Demand

在 Combine 的订阅生命周期中,Subscriber 并不是被动地接收所有数据,而是主动告诉 Publisher 它能处理多少数据。

  1. 建立连接:Subscriber 订阅 Publisher。

  2. 发送订阅对象:Publisher 向 Subscriber 发送一个 Subscription 对象。

  3. 请求数据(核心步骤) :Subscriber 通过 Subscription 调用 request(_ demand: Subscribers.Demand)

    • Demand.unlimited:我要所有数据,你能发多快就发多快。
    • Demand.max(k):我目前只能再处理 kk 个值。
    • Demand.none:我现在不想要数据。
  4. 按需发送:Publisher 只有在收到 Demand 后,才会发送相应数量的值。


2. 动态调整需求

Backpressure 不是一次性的握手,而是一个动态过程。

当 Subscriber 的 receive(_ input:) 函数被触发时,它会返回一个新的 Demand 对象。这个返回值代表**“新增的需求量”**:

  • 如果返回 .max(1),意味着“每收到 1 个值,我允许你再多发 1 个”。
  • 如果返回 .none,需求量保持不变。
  • 注意:需求量是累加的。一旦 Subscriber 请求了 .max(10),Publisher 就获得了发送 10 个值的权限,这个权限无法被撤回。

3. 处理 Backpressure 的常用策略

虽然 Combine 的底层协议支持 Backpressure,但许多内置的 Publisher(如 TimerNotificationCenter)是强制推送的(它们无法停下来等你)。针对这种无法减速的源头,Combine 提供了几种缓冲策略:

A. 缓冲(Buffering)

使用 .buffer 操作符。你可以指定一个缓冲区大小和溢出策略(如丢弃最旧的值或直接报错)。

Swift

publisher
    .buffer(size: 100, prefetch: .byRequest, whenFull: .dropOldest)

B. 节流与防抖(Throttling & Debouncing)

  • Debounce:在一段时间内没有新信号产生时才发送最后一条(常用于搜索框输入)。
  • Throttle:在固定时间窗口内只发送一条数据(常用于按钮高频点击)。

C. 丢弃(Dropping)

使用 .dropFirst() 或类似操作符直接忽略掉某些不重要的数据点。


4. 防御式编程:为什么你的自定义 Subscriber 不动了?

如果你自己实现 Subscriber 协议,最容易犯的错误就是忘记请求数据

  • 陷阱:如果在 receive(subscription:) 中没有调用 subscription.request(.max(n)),Publisher 将永远不会发送任何数据。
  • 结果:你的 pipeline 看起来是通的,但没有任何数据流过。

5. Backpressure 的局限性

虽然 Backpressure 解决了流量控制,但它并不是万能药:

  • 物理限制:如果数据源是硬件传感器(如陀螺仪),它无法因为你的 Demand 而停止产生物理数据。此时 Backpressure 实际上转化为了丢弃策略
  • 逻辑复杂性:过度精细的 Demand 管理会让代码变得难以维护。

总结

Backpressure 保证了系统的稳定性。 在 Combine 中,它将“推”模型转变为“拉”模型,确保了 Subscriber 始终掌握着处理节奏的主动权。