BlockingCollection vs Subject用作消费者

时间:2013-05-11 20:59:23

标签: c# task-parallel-library system.reactive blockingcollection subject

我正在尝试用C#实现一个消费者。有许多发布者可以同时执行。我创建了三个示例,一个使用Rx和subject,一个使用BlockingCollection,第三个使用BlockingCollection中的ToObservable。在这个简单的例子中,它们都做同样的事情,我希望它们与多个生产者一起工作。

每种方法有哪些不同的特质?

我已经在使用Rx,所以我更喜欢这种方法。但我担心OnNext没有线程安全保证,我不知道排队语义是什么主题和默认调度程序。

是否有线程安全主题?

是否要处理所有邮件?

当这不起作用时还有其他任何情况吗?是同时处理?

void SubjectOnDefaultScheduler()
{
    var observable = new Subject<long>();
    observable.
        ObserveOn(Scheduler.Default).
        Subscribe(i => { DoWork(i); });
    observable.OnNext(1);
    observable.OnNext(2);
    observable.OnNext(3);
}

不是Rx,但很容易适应使用/订阅它。它需要一个项目然后处理它。这应该是连续发生的。

void BlockingCollectionAndConsumingTask()
{
    var blockingCollection = new BlockingCollection<long>();
    var taskFactory = new TaskFactory();
    taskFactory.StartNew(() =>
    {
        foreach (var i in blockingCollection.GetConsumingEnumerable())
        {
            DoWork(i);
        }
    });
    blockingCollection.Add(1);
    blockingCollection.Add(2);
    blockingCollection.Add(3);
}

使用阻塞集合有点像主题似乎是一个很好的妥协。我猜是隐式地会安排到任务上,这样我就可以使用async / await,这是正确的吗?

void BlockingCollectionToObservable()
{
    var blockingCollection = new BlockingCollection<long>();
    blockingCollection.
        GetConsumingEnumerable().
        ToObservable(Scheduler.Default).
        Subscribe(i => { DoWork(i); });
    blockingCollection.Add(1);
    blockingCollection.Add(2);
    blockingCollection.Add(3);
}

1 个答案:

答案 0 :(得分:6)

主题不是线程安全的。同时发布的OnNexts将同时直接调用Observer。我个人觉得这很令人惊讶,因为Rx的其他领域在多大程度上强制执行正确的语义。我只能假设这是出于性能考虑而做的。

主题是一种中途宿舍,因为它确实强制终止OnError或OnComplete - 在引发其中任何一个之后,OnNext是NOP。此行为 线程安全。

但是在Subject上使用Observable.Synchronize(),它将强制传出调用遵循正确的Rx语义。特别是,如果同时进行OnNext调用将会阻塞。

底层机制是标准的.NET锁。当锁被多个线程争用时,他们在先到先得的基础上被授予锁定大部分时间。在某些情况下,公平性受到侵犯。但是,您肯定会获得您正在寻找的序列化访问权限。

ObserveOn具有特定于平台的行为 - 如果可用,您可以提供SynchronizationContext和OnNext调用已发布到它。使用调度程序,它最终将调用放到ConcurrentQueue<T>并通过调度程序串行调度 - 因此执行的线程将取决于调度程序。无论哪种方式,排队行为也将强制执行正确的语义。

在这两种情况下(Synchronize&amp; ObserveOn),您当然不会丢失消息。使用ObserveOn,您可以通过选择Scheduler / Context隐式选择要处理消息的线程,使用Synchronize,您将在调用线程上处理消息。哪个更好取决于您的情况。

还有更多需要考虑的因素 - 例如,如果您的生产者超过您的消费者,您想要做什么。

你可能也想看看Rxx Consume:http://rxx.codeplex.com/SourceControl/changeset/view/63470#1100703

示例代码显示了Synchronize行为(Nuget Rx-Testing,Nunit) - 它与Thread.Sleep代码有点混淆,但它非常繁琐而且很糟糕而且我很懒::):

public class SubjectTests
{
    [Test]
    public void SubjectDoesNotRespectGrammar()
    {
        var subject = new Subject<int>();
        var spy = new ObserverSpy(Scheduler.Default);
        var sut = subject.Subscribe(spy);
        // Swap the following with the preceding to make this test pass
        //var sut = subject.Synchronize().Subscribe(spy);

        Task.Factory.StartNew(() => subject.OnNext(1));
        Task.Factory.StartNew(() => subject.OnNext(2));

        Thread.Sleep(2000);

        Assert.IsFalse(spy.ConcurrencyViolation);
    }

    private class ObserverSpy : IObserver<int>
    {
        private int _inOnNext;

        public ObserverSpy(IScheduler scheduler)
        {
            _scheduler = scheduler;
        }

        public bool ConcurrencyViolation = false;
        private readonly IScheduler _scheduler;

        public void OnNext(int value)
        {
            var isInOnNext = Interlocked.CompareExchange(ref _inOnNext, 1, 0);

            if (isInOnNext == 1)
            {
                ConcurrencyViolation = true;
                return;
            }

            var wait = new ManualResetEvent(false);

            _scheduler.Schedule(TimeSpan.FromSeconds(1), () => wait.Set());
            wait.WaitOne();

            _inOnNext = 0;
        }

        public void OnError(Exception error)
        {

        }

        public void OnCompleted()
        {

        }
    }
}