【问题标题】:Rx operator to distinct sequences用于不同序列的 Rx 运算符
【发布时间】:2012-08-11 15:47:18
【问题描述】:

重要提示:有关结果的描述和更多详细信息,请查看我的回答

我需要对通常被复制的一系列对象/事件进行分组和过滤,并以 TimeSpan 间隔对其进行缓冲。我尝试用大理石图更好地解释它:

X-X-X-X-X-Y-Y-Y-Z-Z-Z-Z-X-X-Y-Z-Z

会产生

X---Y---Z---X---Y---Z

其中 X、Y 和 Z 是不同的事件类型,“---”表示间隔。 此外,我还想通过一个关键属性来区分它适用于所有类型,因为它们有一个共同的基类:

X, Y, Z : A

并且 A 包含一个属性 Key。使用符号 X.a 表示 X.Key = a,最终样本将是:

X.a-X.b-X.a-Y.b-Y.c-Z.a-Z.a-Z.c-Z.b-Z.c

会产生

X.a-X.b---Y.b-Y.c-Z.a-Z.c-Z.b

谁能帮我把所需的 Linq 运算符(可能是 DistinctUntilChanged 和 Buffer)放在一起来实现这种行为? 谢谢

更新 18.08.12

根据要求,我尝试给出更好的解释。 我们有设备收集事件并将其发送到 Web 服务。这些设备具有旧逻辑(由于向后兼容性,我们无法更改它)并且它们不断发送事件直到收到确认;确认后,他们发送队列中的下一个事件,依此类推。 事件包含单元的网络地址和一些其他属性,用于区分每个设备的队列中的事件。 事件如下所示:

class Event
{
    public string NetworkAddress { get; }

    public string EventCode { get; }

    public string AdditionalAttribute { get; }
}

目标是每 5 秒处理一次从所有设备接收到的可区分事件,将信息存储在数据库中(这就是我们不想分批执行此操作的原因)并将 ack 发送到设备。 让我们举一个只有两个设备和一些事件的例子:

Device 'a':
Event 1 (a1): NetworkAddress = '1', EventCode = A, AdditionalAttribute = 'x'
Event 2 (a2): NetworkAddress = '1', EventCode = A, AdditionalAttribute = 'y'
Event 3 (a3): NetworkAddress = '1', EventCode = B, AdditionalAttribute = 'x'

Device 'b':
Event 1 (b1): NetworkAddress = '2', EventCode = A, AdditionalAttribute = 'y'
Event 2 (b2): NetworkAddress = '2', EventCode = B, AdditionalAttribute = 'x'
Event 3 (b3): NetworkAddress = '2', EventCode = B, AdditionalAttribute = 'y'
Event 4 (b4): NetworkAddress = '2', EventCode = C, AdditionalAttribute = 'x'

Pn are the operations done by our server, explained later

可能的弹珠图(输入流+输出流):

Device 'a'          : -[a1]-[a1]-[a1]----------------[a2]-[a2]-[a2]-[a3]-[a3]-[a3]-...
Device 'b'          : ------[b1]-[b1]-[b2]-[b2]-[b2]------[b3]-[b3]-[b4]-[b4]-[b4]-...

Time                : ------------[1s]-----------[2s]------------[3s]------------[4s]-
DB/acks (rx output) : ------------[P1]-----------[P2]------------[P3]------------[P4]-

P1: Server stores and acknowledges [a1] and [b1]
P2: "      "      "   "            [b2]
P3: "      "      "   "            [a2] and [b3]
P4: "      "      "   "            [a3] and [b4]

最后我认为这可能是基本运算符的简单组合,但我是 Rx 新手,我有点困惑,因为似乎有很多运算符(或运算符组合)来获得相同的输出流。

19.08.12 更新

请记住,此代码在服务器上运行,并且应该运行数天而不会发生内存泄漏...我不确定受试者的行为。目前,对于每个事件,我都会在服务上调用推送操作,该服务调用主题的 OnNext,我应该在其上构建查询(如果我对主题的使用没有错的话)。

20.08.12 更新

当前实施,包括验证测试;这是我尝试过的,@yamen 建议的似乎相同

public interface IEventService
{
    // Persists the events
    void Add(IEnumerable<Event> events);
}

public class Event
{
    public string Description { get; set; }
}

/// <summary>
/// Implements the logic to handle events.
/// </summary>
public class EventManager : IDisposable
{
    private static readonly TimeSpan EventHandlingPeriod = TimeSpan.FromSeconds(5);

    private readonly Subject<EventMessage> subject = new Subject<EventMessage>();

    private readonly IDisposable subscription;

    private readonly object locker = new object();

    private readonly IEventService eventService;

    /// <summary>
    /// Initializes a new instance of the <see cref="EventManager"/> class.
    /// </summary>
    /// <param name="scheduler">The scheduler.</param>
    public EventManager(IEventService eventService, IScheduler scheduler)
    {
        this.eventService = eventService;
        this.subscription = this.CreateQuery(scheduler);
    }

    /// <summary>
    /// Pushes the event.
    /// </summary>
    /// <param name="eventMessage">The event message.</param>
    public void PushEvent(EventMessage eventMessage)
    {
        Contract.Requires(eventMessage != null);
        this.subject.OnNext(eventMessage);
    }

    /// <summary>
    /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources.
    /// </summary>
    /// <filterpriority>2</filterpriority>
    public void Dispose()
    {
        this.Dispose(true);
    }

    private void Dispose(bool disposing)
    {
        if (disposing)
        {
            // Dispose unmanaged resources
        }

        this.subject.Dispose();
        this.subscription.Dispose();
    }

    private IDisposable CreateQuery(IScheduler scheduler)
    {
        var buffered = this.subject
            .DistinctUntilChanged(new EventComparer())
            .Buffer(EventHandlingPeriod, scheduler);

        var query = buffered
            .Subscribe(this.HandleEvents);
        return query;
    }

    private void HandleEvents(IList<EventMessage> eventMessages)
    {
        Contract.Requires(eventMessages != null);
        var events = eventMessages.Select(this.SelectEvent);
        this.eventService.Add(events);
    }

    private Event SelectEvent(EventMessage message)
    {
        return new Event { Description = "evaluated description" };
    }

    private class EventComparer : IEqualityComparer<EventMessage>
    {
        public bool Equals(EventMessage x, EventMessage y)
        {
            return x.NetworkAddress == y.NetworkAddress && x.EventCode == y.EventCode && x.Attribute == y.Attribute;
        }

        public int GetHashCode(EventMessage obj)
        {
            var s = string.Concat(obj.NetworkAddress + "_" + obj.EventCode + "_" + obj.Attribute);
            return s.GetHashCode();
        }
    }
}

public class EventMessage
{
    public string NetworkAddress { get; set; }

    public byte EventCode { get; set; }

    public byte Attribute { get; set; }

    // Other properties
}

还有测试:

public void PushEventTest()
    {
        const string Address1 = "A:2.1.1";
        const string Address2 = "A:2.1.2";

        var eventServiceMock = new Mock<IEventService>();

        var scheduler = new TestScheduler();
        var target = new EventManager(eventServiceMock.Object, scheduler);
        var eventMessageA1 = new EventMessage { NetworkAddress = Address1, EventCode = 1, Attribute = 4 };
        var eventMessageB1 = new EventMessage { NetworkAddress = Address2, EventCode = 1, Attribute = 5 };
        var eventMessageA2 = new EventMessage { NetworkAddress = Address1, EventCode = 1, Attribute = 4 };
        scheduler.Schedule(() => target.PushEvent(eventMessageA1));
        scheduler.Schedule(TimeSpan.FromSeconds(1), () => target.PushEvent(eventMessageB1));
        scheduler.Schedule(TimeSpan.FromSeconds(2), () => target.PushEvent(eventMessageA1));

        scheduler.AdvanceTo(TimeSpan.FromSeconds(6).Ticks);

        eventServiceMock.Verify(s => s.Add(It.Is<List<Event>>(list => list.Count == 2)), Times.Once());

        scheduler.Schedule(TimeSpan.FromSeconds(3), () => target.PushEvent(eventMessageB1));

        scheduler.AdvanceTo(TimeSpan.FromSeconds(11).Ticks);

        eventServiceMock.Verify(s => s.Add(It.Is<List<Event>>(list => list.Count == 1)), Times.Once());
    }

此外,我再次指出,该软件可以运行数天而不会出现问题,处理数千条消息,这一点非常重要。 明确一点:当前实现没有通过测试。

【问题讨论】:

  • 您的问题X.a-X.b---Y.b-Y.c-Z.a-Z.c-Z.b 中的最后一个序列仅显示一个--- 间隔。这是正确的还是应该在每个值之间进行间隔?
  • 如果您将弹珠图作为源和目标“按比例”提供,或者为我们提供“真实”示例来帮助您,将会很有帮助。
  • 感谢捉迷藏,我会修复输出@yamen 我会添加更多细节
  • 您能否发布一个示例来说明所需的行为与 DistinctUntilChanged 使用键选择器功能给出的行为有何不同?具体来说,您提到了 TimeSpan - 它是如何使用的?

标签: c# system.reactive


【解决方案1】:

我不确定这是否完全符合您的要求,但您可能会使用 group 关键字明确地对元素进行分组,然后在重新组合之前分别操作各种 IObservables。

例如如果我们有类定义,例如

class A
{
    public char Key { get; set; }
}

class X : A { }
...

还有一个Subject&lt;A&gt;

Subject<A> subject = new Subject<A>();

那么我们可以写

var buffered =
    from a in subject
    group a by new { Type = a.GetType(), Key = a.Key } into g
    from buffer in g.Buffer(TimeSpan.FromMilliseconds(300))
    where buffer.Any()
    select new
    {
        Count = buffer.Count,
        Type = buffer.First().GetType().Name,
        Key = buffer.First().Key
    };

buffered.Do(Console.WriteLine).Subscribe();

我们可以用您提供的数据对此进行测试:

subject.OnNext(new X { Key = 'a' }); 
Thread.Sleep(100);
subject.OnNext(new X { Key = 'b' }); 
Thread.Sleep(100);
subject.OnNext(new X { Key = 'a' }); 
Thread.Sleep(100);
...
subject.OnCompleted();

要获得您提供的输出:

{ Count = 2, Type = X, Key = a }
{ Count = 1, Type = X, Key = b }
{ Count = 1, Type = Y, Key = b }
{ Count = 1, Type = Y, Key = c }
{ Count = 2, Type = Z, Key = a }
{ Count = 2, Type = Z, Key = c }
{ Count = 1, Type = Z, Key = b }

【讨论】:

  • 即使不是 100% 准确,我也在这里分配赏金,因为在回答时问题还不够清楚。无论如何,答案是相当有争议的。
【解决方案2】:

不确定这是否正是您想要的,但它似乎支持您的用例。

首先,让我们定义要使用的基类(您可以轻松修改它以满足您的需要):

public class MyEvent
{
    public string NetworkAddress { set; get; }
    public string EventCode { set; get; }
}

让我们将您的设备设置为IObservable&lt;MyEvent&gt; 的数组 - 您可能会以不同的方式使用这些设备,当然需要更改以下内容以适应这种情况。这些设备都会产生一个随机延迟在 0.5 到 1.5 秒之间的值。

var deviceA = new MyEvent[] { new MyEvent() {NetworkAddress = "A", EventCode = "1"},
                              new MyEvent() {NetworkAddress = "A", EventCode = "1"},
                              new MyEvent() {NetworkAddress = "A", EventCode = "2"} };

var deviceB = new MyEvent[] { new MyEvent() {NetworkAddress = "B", EventCode = "1"},
                              new MyEvent() {NetworkAddress = "B", EventCode = "2"},
                              new MyEvent() {NetworkAddress = "B", EventCode = "2"},
                              new MyEvent() {NetworkAddress = "B", EventCode = "3"} };   

var random = new Random();                                 

var deviceARand = deviceA.ToObservable().Select(a => Observable.Return(a).Delay(TimeSpan.FromMilliseconds(random.Next(500,1500)))).Concat();
var deviceBRand = deviceB.ToObservable().Select(b => Observable.Return(b).Delay(TimeSpan.FromMilliseconds(random.Next(500,1500)))).Concat();

var devices = new IObservable<MyEvent>[] { deviceARand, deviceBRand };

现在让我们获取所有这些单独的设备流,使它们“不同”,然后将它们合并为一个主流:

var stream = devices.Aggregate(Observable.Empty<MyEvent>(), (acc, device) => acc.DistinctUntilChanged(a => a.EventCode).Merge(device));

一旦你有了它,让这个流定期被消费只是一个用Buffer缓冲它的问题:

stream.Buffer(TimeSpan.FromSeconds(1)).Subscribe(x => { /* code here works on a list of the filtered events per second */ });

【讨论】:

    【解决方案3】:

    经过搜索和实验,我整理了一些代码,产生了我期望的输出:

    static void Main(string[] args)
        {
            const string Address1 = "A:2.1.1";
            const string Address2 = "A:2.1.2";
            var comparer = new EventComparer();
            var eventMessageA1 = new EventMessage { NetworkAddress = Address1, EventCode = 1, Attribute = 4 };
            var eventMessageB1 = new EventMessage { NetworkAddress = Address2, EventCode = 1, Attribute = 5 };
            var eventMessageA2 = new EventMessage { NetworkAddress = Address1, EventCode = 1, Attribute = 5 };
            var list = new[] { eventMessageA1, eventMessageA1, eventMessageB1, eventMessageA2, eventMessageA1, eventMessageA1 };
    
            var queue = new BlockingCollection<EventMessage>();
            Observable.Interval(TimeSpan.FromSeconds(2)).Subscribe
                (
                    l => list.ToList().ForEach(m =>
                    {
                        Console.WriteLine("Producing {0} on thread {1}", m, Thread.CurrentThread.ManagedThreadId);
                        queue.Add(m);
                    })
                );
    
            // subscribing
            queue.GetConsumingEnumerable()
                .ToObservable()
                 .Buffer(TimeSpan.FromSeconds(5))
                 .Subscribe(e =>
                     {
                         Console.WriteLine("Queue contains {0} items", queue.Count);
                         e.Distinct(comparer).ToList().ForEach(m =>
                      Console.WriteLine("{0} - Consuming: {1} (queue contains {2} items)", DateTime.UtcNow, m, queue.Count));
                     }
                 );
    
            Console.WriteLine("Type enter to exit");
            Console.ReadLine();
        }
    
        public class EventComparer : IEqualityComparer<EventMessage>
        {
            public bool Equals(EventMessage x, EventMessage y)
            {
                var result = x.NetworkAddress == y.NetworkAddress && x.EventCode == y.EventCode && x.Attribute == y.Attribute;
                return result;
            }
    
            public int GetHashCode(EventMessage obj)
            {
                var s = string.Concat(obj.NetworkAddress + "_" + obj.EventCode + "_" + obj.Attribute);
                return s.GetHashCode();
            }
        }
    
        public class EventMessage
        {
            public string NetworkAddress { get; set; }
    
            public byte EventCode { get; set; }
    
            public byte Attribute { get; set; }
    
            public override string ToString()
            {
                const string Format = "{0} ({1}, {2})";
                var s = string.Format(Format, this.NetworkAddress, this.EventCode, this.Attribute);
                return s;
            }
        }
    

    无论如何,监控应用程序,似乎这会导致内存泄漏。 我现在的问题是:

    • 是什么导致了内存泄漏? [请参阅下面的更新]
    • 这是最好的方法吗(如果我将 distinct 放在第一个 observable 上,我不会在下一个缓冲区中获得其他事件,但每个缓冲区中的项目应该与其他项目隔离)?
    • 如何使用测试调度程序编写测试?

    更新

    似乎内存增量只持续了几分钟,然后值是稳定的。我将进行长时间的测试。 当然,这是完全可以接受的行为。

    26.08.12 更新

    • 正如我在之前的更新中已经提到的,内存使用量仅在启动后的几分钟内(并且缓慢地)增加。 8小时后内存消耗稳定,正常波动在几KB范围内)
    • question 与我的非常相似,建议的 Drain 扩展可以很好地解决我的问题(仍有待验证)

    无论如何,我认为我的问题仍然存在于使用测试调度程序进行单元测试

    谢谢 弗朗切斯科

    【讨论】:

      猜你喜欢
      • 2016-08-06
      • 2018-03-14
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多