Combine 框架,从0到1 —— 3.使用 Subscriber 控制发布速度
本文首发于 Ficow Shen's Blog,原文地址: Combine 框架,从0到1 —— 3.使用 Subscriber 控制发布速度。
内容概览
- 前言
- 在发布者生产元素时消耗它们
- 使用自定义的订阅者施加背压(back pressure)
- 使用背压操作符管理无限需求(Unlimited Demand)
- 总结
前言
对于大多数响应式编程场景而言,订阅者不需要对发布过程进行过多的控制。当发布者发布元素时,订阅者只需要无条件地接收即可。但是,如果发布者发布的速度过快,而订阅者接收的速度又太慢,我们该怎么解决这个问题呢?Combine 已经为我们制定了稳健的解决方案!现在,让我们来了解如何施加背压(back pressure,也可以叫反压)以精确控制发布者何时生成元素。
在 Combine 中,发布者生成元素,而订阅者对其接收的元素进行操作。不过,发布者会在订阅者连接和获取元素时才发送元素。订阅者通过 Subscribers.Demand 类型来表明自己可以接收多少个元素,以此来控制发布者发送元素的速率。
订阅者可以通过两种方式来表明需求(Demand):
- 调用
Subscription实例(由发布者在订阅者进行第一次订阅时提供)的request(_:)方法; - 在发布者调用订阅者的
receive(_:)方法来发送元素时,返回一个新的Subscribers.Demand实例;
Demand 是可以累加的。如果订阅者已经请求了两个元素,然后请求 Subscribers.Demand(.max(3)),则现在发布者不满足的需求是五个元素。如果发布者随后发送元素,则未满足的需求将减少到四个。
发布元素是减少未满足需求的数量的唯一方法,订阅者不能请求负需求。
很多应用会使用 sink(receiveValue:) 和 assign(to:on:) 来创建便捷的订阅者类型,分别为:Subscribers.Sink 和 Subscribers.Assign。这两种订阅者在第一次连接到发布者时,会发送一个 unlimited 的 Demand,这时候订阅者会一直不停地接收发布者发来的内容。
在发布者生产元素时消耗它们
当发布者的需求很高或不受限制时,它发送元素的速度可能比订阅者处理元素的速度快很多。这种情况可能导致元素丢失,或者在元素等待被缓存时迅速增加内存的压力。
如果您使用便捷的订阅者,则会发生这种情况,因为它们的需求(Demand) 是无限数量 (unlimited) 的元素。确保您提供给 sink(receiveValue:) 的闭包和 assign(to:on:) 的副作用(执行效果)遵循以下特征:
- 不会阻塞发布者;
- 不会因为缓存元素而消耗过多的内存;
- 不会不知所措并且不能处理元素;
庆幸的是,许多常用的发布者(例如与用户界面元素相关联的发布者)都会以可控的速度进行发布。其他常见的发布者仅仅生成一个元素,例如:URL 加载系统的 URLSession.DataTaskPublisher。配合这些发布者,使用 sink(receiveValue:) 和 assign(to:on:) 订阅者是绝对安全的。
使用自定义的订阅者施加背压(back pressure)
想要控制发布者向订阅者发送元素的速率,可以创建订阅者协议的自定义实现。使用你的自定义实现来指定你的订阅者可以适应的需求。当订阅者接收元素时,它可以通过返回新的需求值给 receive(_:) 方法,或通过在订阅上调用 request(_:) 来请求更多内容。无论使用哪种方法,你自定义的订阅者都可以在任何给定时间微调发布者可以发送的元素数量。
通过发信号来表明订阅者已准备好接收元素来控制流量的概念称为
背压。
每个发布者都跟踪其当前未满足的需求,也就是:订阅者已请求多少个元素。甚至,像 Foundation 框架中的 Timer.TimerPublisher 这样的自动化资源,也只会在有未满足的需求时才产生元素。
下面的示例代码说明了这个行为:
// 发布者: 使用一个定时器来每秒发送一个日期对象
let timerPub = Timer.publish(every: 1, on: .main, in: .default)
.autoconnect()
// 订阅者: 在订阅以后,等待5秒,然后请求最多3个值
class MySubscriber: Subscriber {
typealias Input = Date
typealias Failure = Never
var subscription: Subscription?
func receive(subscription: Subscription) {
print("published received")
self.subscription = subscription
DispatchQueue.main.asyncAfter(deadline: .now() + 5) {
subscription.request(.max(3))
}
}
func receive(_ input: Date) -> Subscribers.Demand {
print("\(input) \(Date())")
return Subscribers.Demand.none
}
func receive(completion: Subscribers.Completion<Never>) {
print ("--done--")
}
}
// 订阅 timerPub
let mySub = MySubscriber()
print ("Subscribing at \(Date())")
timerPub.subscribe(mySub)
订阅者的 receive(subscription:) 实现在请求发布者的任何元素之前执行了五秒钟的延迟。在此期间,发布者存在并具有有效的订阅者,但需求为零,因此不会产生任何元素。它仅在延迟到期且订阅者给它一个非零需求 subscription.request(.max(3)) 之后才开始发布元素,如以下输出所示:
Subscribing at 2019-12-09 18:57:06 +0000
published received
2019-12-09 18:57:11 +0000 2019-12-09 18:57:11 +0000
2019-12-09 18:57:12 +0000 2019-12-09 18:57:12 +0000
2019-12-09 18:57:13 +0000 2019-12-09 18:57:13 +0000
这个示例只请求了三个元素,在五秒钟的延迟到期后发出需求。最后,发布者在第三个元素之后不再发送其他元素,但是也不会通过发送完成(.finished) 的值来完成发布,因为发布者只是在等待更多需求。为了继续接收元素,订阅者可以存储订阅并定期请求更多元素。它还可以在 receive(_:) 方法中返回新需求的值。
使用背压操作符管理无限需求(Unlimited Demand)
即使没有自定义的订阅者,你也可以通过一些操作符来实施背压:
buffer(size:prefetch:whenFull:),保留来自上游发布者的固定数量的项目。缓冲满了之后,缓冲区会丢弃元素或抛出错误;debounce(for:scheduler:options:),只在上游发布者在指定的时间间隔内停止发布时才发布;throttle(for:scheduler:latest:),以给定的最大速率生成元素。如果在一个间隔内接收到多个元素,则仅发送最新的或最早的元素;collect(_:)和collect(_:options:)聚集元素,直到它们超过给定的数量或时间间隔,然后向订阅者发送元素数组。如果订阅者可以同时处理多个元素,这个操作符将是很好的选择。
由于这些操作符可以控制订阅者接收的元素数量,因此可以放心地连接无限需求的订阅者,例如:sink(receiveValue:) 和 assign(to:on:)。
总结
通过实施背压,我们可以灵活地调控发布过程。背压操作符可以帮助我们应对大多数场景,这些操作符可以大幅提升我们的开发效率。
比如这种常见的场景:当搜索输入框的内容发生变动时,应用需要去查找用户输入内容对应的结果,但是这个查找操作的频率需要有一定的控制。如果用户按住一个键不放开,输入框的内容就会一直变化,此时就会触发多次查找操作。这时候,我们可以从容地使用背压操作符解决这种问题。
如果你需要处理的场景非常复杂,通过自定义订阅者来实施精确的背压将会是一个更好的选择。
本文内容来源: Processing Published Elements with Subscribers,转载请注明出处。
Combine 框架,从0到1 —— 3.使用 Subscriber 控制发布速度的更多相关文章
- Combine 框架,从0到1 —— 1.核心概念
本文首发于 Ficow Shen's Blog,原文地址: Combine 框架,从0到1 -- 1.核心概念. 内容概览 前言 核心概念 RxSwift Combine 总结 参考内容 ...
- Combine 框架,从0到1 —— 2.通过 ConnectablePublisher 控制何时发布
本文首发于 Ficow Shen's Blog,原文地址: Combine 框架,从0到1 -- 2.通过 ConnectablePublisher 控制何时发布. 内容概览 前言 使用 ma ...
- Combine 框架,从0到1 —— 4.在 Combine 中使用通知
本文首发于 Ficow Shen's Blog,原文地址: Combine 框架,从0到1 -- 4.在 Combine 中使用通知. 内容概览 前言 让通知处理代码使用 Combine 总结 ...
- Combine 框架,从0到1 —— 4.在 Combine 中使用计时器
本文首发于 Ficow Shen's Blog,原文地址: Combine 框架,从0到1 -- 4.在 Combine 中使用计时器. 内容概览 前言 使用计时器执行周期性的工作 将计时器转换为计时 ...
- Combine 框架,从0到1 —— 4.在 Combine 中使用 KVO
本文首发于 Ficow Shen's Blog,原文地址: Combine 框架,从0到1 -- 4.在 Combine 中使用 KVO. 内容概览 前言 用 KVO 监控改动 将 KVO 代 ...
- Combine 框架,从0到1 —— 4.在 Combine 中执行异步代码
本文首发于 Ficow Shen's Blog,原文地址: Combine 框架,从0到1 -- 4.在 Combine 中执行异步代码. 内容概览 前言 用 Future 取代回调闭包 用输出类型( ...
- Combine 框架,从0到1 —— 5.Combine 提供的发布者(Publishers)
本文首发于 Ficow Shen's Blog,原文地址: Combine 框架,从0到1 -- 5.Combine 提供的发布者(Publishers). 内容概览 前言 Just Future D ...
- Combine 框架,从0到1 —— 5.Combine 中的 Subjects
本文首发于 Ficow Shen's Blog,原文地址: Combine 框架,从0到1 -- 5.Combine 中的 Subjects. 内容概览 前言 PassthroughSubject C ...
- Combine 框架,从0到1 —— 5.Combine 常用操作符
本文首发于 Ficow Shen's Blog,原文地址: Combine 框架,从0到1 -- 5.Combine 常用操作符. 内容概览 前言 print breakpoint handleEve ...
随机推荐
- PHP date_date_set() 函数
------------恢复内容开始------------ 实例 返回一个新的 DateTime 对象,设置一个新的日期,然后格式化日期: <?php$date=date_create();d ...
- PHP filectime() 函数
定义和用法 filectime() 函数返回指定文件的上次修改时间. 该函数将检查文件的日常修改情况和 inode 修改情况.inode 修改情况是指:权限的修改.所有者的修改.用户组的修改或其他元数 ...
- PHP xpath() 函数
定义和用法 xpath() 函数运行对 XML 文档的 XPath 查询.高佣联盟 www.cgewang.com 如果成功,该函数返回 SimpleXMLElements 对象的一个数组.如果失败, ...
- OpenCL 增强单work-item kernel性能策略
1.基于反馈的Optimization Report解决单个Work-item的Kernel相关性 在许多情况下,将OpenCL™应用程序设计为单个工作项内核就足以在不执行其他优化步骤的情况下最大化性 ...
- 构建一个拥有sshd服务的docker镜像
不直接描述结果,通过一个过程探究如何写一个 Dockerfile 一.环境 虚拟机CentOS7.4,Docker1.13.1 二.尝试步骤 1.下载基础镜像 docker pull alpine:3 ...
- CF1349F 【Slime and Sequences】part1
由于本文过长,\(\LaTeX\) 炸了,分两篇,part2 题目描述 定义一个正整数序列为好序列,当且仅当如果某个数 \(k\) 出现过,那么一定有 \(k-1\) 在最后一个 \(k\) 的前面出 ...
- 阿里ECS云服务器部署文件
今天,接触了阿里ECS云服务器,免费领取链接https://dwz.cn/WOFZpZz1 获取之后,要添加一下端口,刚开始需要80 8080 3306的端口,其他的根据需要自行添加 点击快速创建 ...
- Spring 中的反射与反射的原理
作者:DeppWang.原文地址 在造轮子:实现一个简易的 Spring IoC 容器一文中提到 Spring 在创建 Bean 实例和依赖注入时使用了反射,本文来具体分析一下 Spring 中的反射 ...
- javascript函数的笔记
1.函数的概念 封装一段可以被重复调用执行的代码块来实现大量代码的重复使用 2.函数的使用分为两步:声明函数 和 调用函数 3.声明函数的关键字全部是小写 4.函数名一 ...
- PyTorch 学习
PyTorch torch.autograd模块 深度学习的算法本质上是通过反向传播求导数, PyTorch的autograd模块实现了此功能, 在Tensor上的所有操作, autograd都会为它 ...