推送模型和拉取模型之间有什么区别,例如IEnumerable<T>和IObservable<T>?

35
在每个关于IEnumerableIObservable的技术讲座或博客文章中,我都读到了这样的说法:IEnumerable是一个拉取式结构,而IObservable是一个推送式结构。
我读到说,使用IObservable时,我们有异步调用,没有阻塞,一切都是推送式的。
但是,但是,但是...
这到底意味着什么?什么是PushPull式的结构?
因为在我的看法中,在IEnumerable中我们也可以将数据推送到结构中并从中拉取数据,我真的在那些技术术语和概念中迷失了方向。
请用普通人易懂的方式向我解释这两种结构之间的区别以及推送式和拉取式结构之间的区别。
谢谢。
4个回答

56

请用通俗易懂的方式解释一下它们之间的区别。

好的,让我们来烤面包吧。这是我最正常、最人性化的行为。

推式:

// I want some toast.  I will pull it out of the toaster when it is done.
// I will do nothing until the toast is available.
Toast t = toaster.MakeToast();
t.AddJam();
// Yum.

基于推送:

// I want some toast.  But it might take a while and I can do more work
// while I am waiting:
Task<Toast> task = toaster.MakeToastAsync();
Toast t = await task;
// await returns to the caller if the toast is not ready, and assigns
// a callback. When the toast is ready, the callback causes this method
// to start again from this point:
t.AddJam();
// Yum.

当你想要获取结果时,你调用一个函数并等待函数返回。

当你想要接收结果时,你调用一个异步函数并等待结果。当结果可用时,它会被推送到回调函数中,然后该方法恢复执行。

IEnumerable<T>只是一系列pulls的序列;每次想要拉取结果时,你都需要调用MoveNext并获取T。而IObservable<T>则是一系列pushes的序列;你注册一个回调函数,每当有新的T可用时就会调用该回调函数。

换句话说:IEnumerable<T>在逻辑上是一系列Func<T>的调用。而IObservable<T>在逻辑上是一系列Task<T>的延续。不要让它们是“序列”这个事实误导你;这只是附带的。其基本思想是函数是同步的;你调用它们并同步获取结果;如果需要等待,就等待。任务是异步的;你启动它们并在结果可用时异步获取结果。


IObservableawait出现之前,这个思想已经存在于C#中。另一种看待它的方式是:pull-based就像函数调用,push-based就像事件处理程序。普通的函数调用是在需要时调用的。而事件处理程序则是在发生事件时调用你。事件是C#语言本身表示观察者模式的方式。但是,事件始终逻辑上形成一个序列,因此我们可以像操作拉取项的序列一样操作推送项的序列。因此,IObservable被发明了出来。


13
现在我想要吐司。 - Sach
5
我曾经在这个话题上看到的最好的答案之一。非常感谢,我现在会以异步方式制作吐司。 - Tornike Gomareli
3
我通常用热风枪来烤面包片,这时不能分心做其他事情,否则面包片就会烤焦。我不想冒险尝试其他方法。你的回答很出色! - user2819245
@elgonzo,你可以通过购买实现IAsyncEnumerable接口的热风枪来避免放弃你的热风枪而选择可观察的烤面包机。 - Sentinel
1
Eric,我有点怀疑将IObservable<T>与一系列Task<T>延续进行比较。使用可观察对象,您只需订阅一次即可获得多个通知。而对于任务序列,您必须单独订阅每个任务(通过等待它),以从每个订阅中获取单个通知。我还想知道您对IAsyncEnumerable的看法。我很困惑这些应该被视为拉或推结构。似乎两种观点都有论据。 - Theodor Zoulias

12
一个男人走进杂货店,问店主有没有鸡蛋。"有的," 店主说。 "我可以要一些吗?" 男人问道。 然后店主给了男人一些鸡蛋。 "你还有更多吗?" 男人问。 "有的," 店主说。 "我可以要一些吗?" 男人又问道。 然后店主给了男人一些鸡蛋。 "你还有更多吗?" 男人继续问。 "没有了," 店主说。于是男人离开了。
那是基于拉取的方式。这个男人不停地从店主那里"拉取"鸡蛋,直到没有了为止。
一个男人走进杂货店,问店主能否送鸡蛋,并询问是否可以在任何时候送到。 "可以," 店主说。男人离开了。几天后,一些鸡蛋送到了。再过几天,又有一些鸡蛋送到了。然后男人打电话给店主,要求停止送货,此后就再也没有鸡蛋送到了。
那是基于推动的方式。男人不等店主给他鸡蛋,而是去做其他事情,店主把鸡蛋"推"给了男人。

10
假设有一个(逻辑)服务器和一个客户端,谁确定数据何时传递,服务器还是客户端?
基于拉取的方案是由客户端运行的:客户端发送请求,然后立即提供数据。基于推送的方案是由服务器运行的:客户端可以连接到推送流(IObservable),但不能要求数据,只有在服务器愿意提供时才会收到数据。
拉取的规范形式是数据库查询:您向服务器发送请求,服务器响应一组项目。推送的规范版本是聊天应用程序:聊天客户端无法“要求”新对话数据,只有当对方说话时服务器才会通知您。

另一个很棒的答案。非常感谢,聊天示例很棒。 - Tornike Gomareli

2
除了以上的好回答,我还想提供以下内容:
  • 'IEnumerable' 的意思是 '可枚举的',在 .NET 框架概念模型中被隐式地与 '拉取式' 混淆。它的意图是指 "允许你获取一个枚举器,该枚举器允许你 拉取 下一个值"
  • 但我们也有 IAsyncEnumerable。
  • 从数学上讲,'enumerable' 只是表示 '可计数的',即可数集合。
  • Observables 也可能是可数的,因为一个 observable 可能代表一组离散事件。

命名混乱,我认为它们并没有很好地表达它们打算传达的概念。例如,在 Java 世界中,IEnumerable 是 Iterable,更接近于 .NET 中的意图。

我认为最简单的方法是将 IEnumerable 和围绕它们的 LINQ 结构想象成 "从某个源获取一些数据,并按照这种方式进行过滤/分组..." 而 Observables 则可以被认为是可以反应的输入流。我认为将 observables 视为连续体并不一定有帮助。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接