为什么在.NET Reactive Extensions中不推荐主题?


111

我目前正在掌握.NET的Reactive Extensions框架,并且正在通过各种发现的资源(主要是http://www.introtorx.com)进行工作。

我们的应用程序涉及许多检测网络帧的硬件接口,这些接口将是我的IObservables,然后,我将拥有各种组件,这些组件将消耗这些帧或对数据执行某种形式的转换并生成一种新型的帧。例如,还有其他组件需要显示第n个帧。我坚信Rx将对我们的应用程序有用,但是我正在为IObserver接口的实现细节而苦苦挣扎。

我正在阅读的大多数(如果不是全部)资源都说我不应该自己实现IObservable接口,而是使用所提供的函数或类之一。从我的研究看来,创建a Subject<IBaseFrame>将满足我的需求,我将拥有一个从硬件接口读取数据然后调用Subject<IBaseFrame>实例的OnNext函数的单线程。然后,不同的IObserver组件将从该主题接收其通知。

我的困惑来自本教程附录中的建议:

避免使用主题类型。Rx实际上是一种功能编程范例。使用主体意味着我们现在正在管理状态,该状态可能会发生变异。很难同时处理变异状态和异步编程。此外,许多操作符(扩展方法)都经过精心编写,以确保维持正确和一致的订阅和序列生存期。介绍主题时,您可以打破这一点。如果您明确使用主题,将来的版本可能还会看到性能显着下降。

我的应用程序对性能至关重要,显然,我将在将Rx模式用于生产代码之前测试其性能。但是,我担心通过使用Subject类所做的事情与Rx框架的精神背道而驰,并且该框架的未来版本将损害性能。

有做我想要的更好的方法吗?不管是否有观察者,硬件轮询线程都将连续运行(否则将备份硬件缓冲区),因此这是一个非常热的序列。然后,我需要将接收到的帧传递给多个观察者。

任何建议将不胜感激。


1
它确实帮助我理解了该主题,我对如何在应用程序中使用它一目了然。我知道它们是对的-我有一个非常面向推送的组件管道,我需要对UI线程进行各种过滤和调用以在GUI中显示,以及缓冲最后收到的帧等等等-我只需要确保我第一次做就对!
安东尼

Answers:


70

好的,如果我们忽略我的教条式方法,而同时忽略“对象是好是坏”。让我们看一下问题空间。

我敢打赌,您要么拥有2种风格中的1种,要么就需要融入其中。

  1. 消息到达时,系统引发事件或回叫
  2. 您需要轮询系统以查看是否有任何消息要处理

对于选项1,很简单,我们只需要使用适当的FromEvent方法包装它就可以了。到酒吧!

对于选项2,我们现在需要考虑如何对此进行轮询以及如何有效地进行此操作。另外,当我们获得价值时,如何发布它?

我想您会需要一个专用的轮询线程。您不希望其他编码器重击ThreadPool / TaskPool并使您陷入ThreadPool饥饿的境地。另外,您也不需要上下文切换的麻烦(我想)。因此,假设我们有自己的线程,我们可能会有某种While / Sleep循环供我们轮询。当支票找到一些消息时,我们将其发布。好吧,所有这些对于Observable.Create来说都是完美的。现在我们可能无法使用While循环,因为那样将不允许我们返回Disposable以允许取消。幸运的是,您已经阅读了整本书,因此精通递归调度!

我想象这样的事情可能会起作用。#未测试

public class MessageListener
{
    private readonly IObservable<IMessage> _messages;
    private readonly IScheduler _scheduler;

    public MessageListener()
    {
        _scheduler = new EventLoopScheduler();

        var messages = ListenToMessages()
                                    .SubscribeOn(_scheduler)
                                    .Publish();

        _messages = messages;
        messages.Connect();
    }

    public IObservable<IMessage> Messages
    {
        get {return _messages;}
    }

    private IObservable<IMessage> ListenToMessages()
    {
        return Observable.Create<IMessage>(o=>
        {
                return _scheduler.Schedule(recurse=>
                {
                    try
                    {           
                        var messages = GetMessages();
                        foreach (var msg in messages)
                        {
                            o.OnNext(msg);
                        }   
                        recurse();
                    }
                    catch (Exception ex)
                    {
                        o.OnError(ex);
                    }                   
                });
        });
    }

    private IEnumerable<IMessage> GetMessages()
    {
         //Do some work here that gets messages from a queue, 
         // file system, database or other system that cant push 
         // new data at us.
         // 
         //This may return an empty result when no new data is found.
    }
}

我真的不喜欢Subjects的原因是通常情况下,开发人员并未对此问题进行清晰的设计。修改一个主题,在这里和任何地方戳它,然后让可怜的支持开发人员猜测WTF正在进行中。当您使用创建/生成等方法时,您将在序列上定位效果。您可以通过一种方法看到全部内容,并且您知道没有其他方法会带来讨厌的副作用。如果我看到一个学科领域,我现在必须去寻找一个正在使用的班级中的所有地方。如果某个MFer公开曝光,那么所有下注都将被取消,谁知道该顺序的使用方式!异步/并发/接收很难。您无需通过允许进行副作用和因果关系编程来使您的头脑更加旋转的方式来加倍努力。


10
我现在正在阅读此答案,但是我觉得应该指出,我永远不会考虑公开Subject接口!我正在使用它在密封类(公开IObservable <>)中提供IObservable <>实现。我绝对可以理解为什么公开Subject <>接口会是Bad Thing™
Anthony

嘿,很抱歉,但是我实在不太了解您的代码。ListenToMessages()和GetMessages()在做什么并返回?
user10479

1
对于您的个人项目@jeromerg,这可能很好。然而,以我的经验,开发人员在WPF,MVVM,单元测试GUI设计以及随后加入Rx方面会遇到很多困难。我尝试了BehaviourSubject-as-a-property模式。但是,我发现,如果我们使用标准的INPC属性,然后使用简单的Extension方法将其转换为IObservable,则它对于其他人更容易采用。此外,您将需要自定义WPF绑定才能与您的行为主题一起使用。现在,您可怜的团队也必须学习WPF,MVVM,Rx和新框架。
李·坎贝尔

2
@LeeCampbell,就您的代码示例而言,通常的方式是MessageListener由系统构造(您可能以某种方式注册了类名),并且被告知系统随后将调用OnCreate()并OnGoodbye(),并在生成消息时调用message1(),message2()和message3()。好像messageX [123]会在某个主题上调用OnNext,但是有更好的方法吗?
詹姆斯·摩尔

1
@JamesMoore,因为用具体示例更容易解释这些内容。如果您知道使用Rx和Subjects的开源Android应用程序,那么也许我可以花些时间看看我是否可以提供更好的方法。我确实知道站在基座上并说主体不好是没有太大帮助的。但是我认为IntroToRx,RxCookbook和ReactiveTrader之类的东西都提供了使用Rx的各种示例。
李·坎贝尔

38

通常,您应该避免使用Subject,但是对于您在这里所做的事情,我认为它们效果很好。我问了类似的问题当我在Rx教程中遇到“避免主题”消息时,。

引用Dxx Sexton(来自Rxx)

“主题是Rx的状态组件。当您需要创建类似事件的可观察字段或局部变量时,它们非常有用。”

我倾向于将它们用作Rx的入口点。因此,如果我有一些需要说“发生了什么事”的代码(如您所说),则可以使用Subjectand调用OnNext。然后将其公开IObservable给其他人订阅(您可以AsObservable()在您的主题上使用,以确保没有人可以强制转换为主题并弄乱事情)。

您也可以通过.NET事件并使用来实现此目的FromEventPattern,但是如果我IObservable无论如何仅将事件转换为事件,则看不到使用事件而不是使用事件的好处Subject(这可能意味着我丢失了这里的东西)

不过,你应该避免非常强烈被预订至IObservable一个Subject,即没有传递SubjectIObservable.Subscribe方法。


为什么根本需要状态?如我的回答所示,如果将问题分解为多个部分,则实际上根本不需要管理状态。在这种情况下,不应使用主题。
casperOne 2013年

8
@casperOne您不需要在Subject <T>或事件(它们都有要调用的东西,观察者或事件处理程序的集合)之外的状态。如果添加事件的唯一原因是用FromEventPattern包装它,我只是喜欢使用Subject。除了对您可能很重要的异常示意图的更改之外,以这种方式避免使用Subject不会带来任何好处。同样,在这里我可能会错过其他一些比主题更好的事件。提及国家只是报价的一部分,而似乎最好保留它。也许没有那一部分会更清楚吗?
威尔卡,

@casperOne-但您也不应仅使用FromEventPattern包装事件。这显然是一个可怕的想法。
詹姆斯·摩尔

3
我已经在此博客文章中更深入地解释了我的报价。
戴夫·塞克斯顿

我倾向于将它们用作Rx的入口点。这对我来说是致命的一击。我遇到的情况是,有一个API在被调用时会生成我想通过反应式处理管道传递的事件。主题是我的答案,因为FromEventPattern在RxJava AFAICT中似乎不存在。
scorpiodawg

31

通常,在管理主题时,实际上实际上是在重新实现Rx中已经存在的功能,并且可能不是以健壮,简单和可扩展的方式实现。

当您尝试将一些异步数据流适配到Rx中(或从当前非异步的异步数据流创建异步数据流)时,最常见的情况通常是:

  • 数据源是一个事件:正如Lee所说的,这是最简单的情况:使用FromEvent并前往酒吧。

  • 数据源来自同步操作,并且您需要轮询更新(例如,Web服务或数据库调用):在这种情况下,您可以使用Lee的建议方法,或者在简单情况下,可以使用Observable.Interval.Select(_ => <db fetch>)。当源数据中没有任何更改时,您可能希望使用DistinctUntilChanged()阻止发布更新。

  • 数据源是某种异步api,它会调用您的回调:在这种情况下,请使用Observable.Create挂接您的回调,以在观察者上调用OnNext / OnError / OnComplete。

  • 数据源是一个阻塞,直到新数据可用(例如某些同步套接字读取操作)的调用:在这种情况下,可以使用Observable.Create封装从套接字读取并发布到Observer.OnNext的命令性代码。读取数据时。这可能类似于您对主题所做的事情。

使用Observable.Create与创建一个管理Subject的类相对于使用yield关键字与创建一个实现IEnumerator的整个类相当。当然,您可以编写IEnumerator使其与yield代码一样干净,公民身份良好,但是哪个封装得更好并且感觉更整洁?Observable.Create与管理Subject的情况相同。

Observable.Create为您提供了一种干净的模式,可以进行懒惰的安装和拆卸。您如何通过包装主题的类来实现这一目标?您需要某种Start方法...您如何知道何时调用它?还是即使没有人在听,还是只是总是启动它?当您完成操作后,如何停止从套接字读取信息/轮询数据库等?您必须具有某种Stop方法,并且不仅要访问已预订的IObservable,而且还要有权访问首先创建Subject的类。

使用Observable.Create,将它们全部包装在一个地方。在有人订阅之前,Observable.Create的主体不会运行,因此,如果没有人订阅,则永远不会使用您的资源。并且Observable.Create返回一个Disposable,该Disposable可以完全关闭您的资源/回调等,这在Observer取消订阅时被调用。您用于生成Observable的资源的生存期与Observable本身的生存期紧密相关。


1
Observable.Create的解释非常清楚。谢谢!
埃文·莫兰

1
在某些情况下,我还会使用代理对象公开可观察对象(例如,它只是可变属性)的主题。不同的组件将调用代理,以告知该属性何时更改(使用方法调用),并且该方法执行OnNext。消费者订阅。我想我会在这种情况下使用BehaviorSubject,是否合适?
Frank Schwieterman

1
这取决于实际情况。良好的Rx设计趋向于将系统转变为异步/反应式体系结构。很难将反应式代码的小组件与必须进行设计的系统完美集成。创可贴解决方案是使用主题将命令性操作(函数调用,属性集)转换为可观察的事件。这样一来,您最终只能获得一小部分反应式代码,而没有真正的“啊哈!” 时刻。更改设计以对数据流建模并对其做出反应通常可以提供更好的设计,但这是一种普遍的变化,需要心态转变和团队支持。
Niall Connaughton

1
我在这里(由于没有经验的Rx)在此声明:通过使用Subject,您可以在一个增长的命令式应用程序中进入Rx的世界,并对其进行缓慢的转换。也是为了获得初步经验……。当然,稍后再将代码更改为从一开始就应该是这样(大声笑)。但首先,我认为使用主题可能是值得的。
罗贝托

9

带引号的块文本在很大程度上解释了为什么不应该使用Subject<T>,但是为了简化起见,您将观察者和可观察的功能结合在一起,同时在两者之间注入某种状态(无论是封装还是扩展)。

这是您遇到麻烦的地方;这些责任应该是分开的并且彼此不同。

也就是说,在您的特定情况下,我建议您将您的担忧分解为较小的部分。

首先,您的线程很热,并且始终监视硬件是否有信号来发出通知。您通常会如何做? 大事记。因此,让我们开始吧。

让我们定义EventArgs您的事件将触发。

// The event args that has the information.
public class BaseFrameEventArgs : EventArgs
{
    public BaseFrameEventArgs(IBaseFrame baseFrame)
    {
        // Validate parameters.
        if (baseFrame == null) throw new ArgumentNullException("IBaseFrame");

        // Set values.
        BaseFrame = baseFrame;
    }

    // Poor man's immutability.
    public IBaseFrame BaseFrame { get; private set; }
}

现在,将触发事件的类。请注意,这可能是一个静态类(因为你总是有一个线程中运行监控硬件缓存),或调用的按需订用的东西。您必须对此进行适当的修改。

public class BaseFrameMonitor
{
    // You want to make this access thread safe
    public event EventHandler<BaseFrameEventArgs> HardwareEvent;

    public BaseFrameMonitor()
    {
        // Create/subscribe to your thread that
        // drains hardware signals.
    }
}

因此,现在您有了一个公开事件的类。观察值与事件配合良好。如此之多,以至于IObservable<T>如果您遵循标准事件模式,通过上的静态FromEventPattern方法,将一流的支持将事件流(将事件流视为一个事件的多次触发)转换为实现Observable

使用事件的源和FromEventPattern方法,我们可以IObservable<EventPattern<BaseFrameEventArgs>>轻松地创建一个EventPattern<TEventArgs>该类体现了您在.NET事件中看到的内容,尤其是从NET派生的实例EventArgs和代表发送者的对象),如下所示:

// The event source.
// Or you might not need this if your class is static and exposes
// the event as a static event.
var source = new BaseFrameMonitor();

// Create the observable.  It's going to be hot
// as the events are hot.
IObservable<EventPattern<BaseFrameEventArgs>> observable = Observable.
    FromEventPattern<BaseFrameEventArgs>(
        h => source.HardwareEvent += h,
        h => source.HardwareEvent -= h);

当然,您想要一个IObservable<IBaseFrame>,但这很简单,使用类上的Select扩展方法Observable来创建投影(就像您在LINQ中一样,我们可以将所有这些包装在一个易于使用的方法中):

public IObservable<IBaseFrame> CreateHardwareObservable()
{
    // The event source.
    // Or you might not need this if your class is static and exposes
    // the event as a static event.
    var source = new BaseFrameMonitor();

    // Create the observable.  It's going to be hot
    // as the events are hot.
    IObservable<EventPattern<BaseFrameEventArgs>> observable = Observable.
        FromEventPattern<BaseFrameEventArgs>(
            h => source.HardwareEvent += h,
            h => source.HardwareEvent -= h);

    // Return the observable, but projected.
    return observable.Select(i => i.EventArgs.BaseFrame);
}

7
感谢您的回复@casperOne,这是我最初的方法,但是添加事件以使我可以用Rx包装它“感觉不对”。我目前使用委托(是的,我知道这就是一个事件!)来适应用于加载和保存配置的代码,这必须能够重建组件管道,并且委托系统给了我最多灵活性。Rx现在在这方面让我头疼,但是框架中其他所有功能的强大功能都使解决配置问题非常值得。
安东尼

@Anthony如果可以使他的代码示例正常工作,那很好,但是正如我评论的那样,这没有任何意义。至于“错误”的感觉,我不知道为什么将事物细分为逻辑部分似乎是“错误的”,但是您在原始文章中没有提供足够的详细信息来表明如何最好地将其翻译IObservable<T>为关于您如何处理的信息。当前给出该信息的信令。
casperOne 2013年

@casperOne您认为,主题的使用是否适合于消息总线/事件聚合器?
kitsune

1
@kitsune不,我不明白他们为什么会这么做。如果您正在考虑“优化”,则必须问是否是问题所在,是否已将Rx视为问题的起因?
casperOne 2013年

2
我在这里同意casperOne的观点,将关注点拆分是一个好主意。我想指出的是,如果使用“硬件到事件到Rx”模式,则会丢失错误语义。任何丢失的连接或会话等都不会暴露给消费者。现在,消费者无法决定是否要重试,断开连接,订阅其他序列或其他内容。
Lee Campbell

0

笼统地说,主题不适用于公共界面是不好的。尽管这确实是对的,但这并不是反应式编程方法应具有的样子,但对于经典代码而言,它绝对是一个不错的改进/重构选项。

如果您具有带有公共集访问器的普通属性,并且想通知有关更改,那么对于将其替换为BehaviorSubject毫无疑问。INPC或其他事件不是很干净,并且使我感到厌倦。为此,您可以并且应该将BehaviorSubjects用作公共属性,而不是常规属性和沟INPC或其他事件。

此外,主题界面使界面的用户更了解属性的功能,并且更有可能订阅而不是仅仅获取价值。

如果您希望其他人收听/订阅属性更改,则最好使用该属性。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.