抛出可观察的LINQ不一致异常

时间:2016-02-01 22:11:02

标签: c# .net linq system.reactive observable

在我编写股票市场交易员IObserver的过程中,我遇到了三个错误,主要是从Reactive Extensions库中抛出。

我有以下CompanyInfo课程:

public class CompanyInfo
{
    public string Name { get; set; }

    public double Value { get; set; }
}

IObservable<CompanyInfo>名为StockMarket

public class StockMarket : IObservable<CompanyInfo>

我的Observer如下所示:

public class StockTrader : IObserver<CompanyInfo>
{
    public void OnCompleted()
    {
        Console.WriteLine("Market Closed");
    }

    public void OnError(Exception error)
    {
        Console.WriteLine(error);
    }

    public void OnNext(CompanyInfo value)
    {
        WriteStock(value);
    }

    private void WriteStock(CompanyInfo value) { ... }
}

我运行以下代码:

StockMarket market = GetStockMarket();
StockTrader trader = new StockTrader();

IObservable<CompanyInfo> differential = market  //[F, 1], [S, 5], [S, 4], [F, 2]
    .GroupBy(x => x.Name)                       //[F, 1], [F, 2]; [S, 5], [S, 4]
    .SelectMany(x => x                  //4, 8, 2, 3
        .Buffer(2, 1)                   //(4, 8), (8, 2), (2, 3), (3)
        .SkipLast(1)                    //(4, 8), (8, 2), (2, 3)
        .Select(y => new CompanyInfo    //(+100%), (-75%), (+50%)
        {
            Name = x.Key,
            Value = (y[1].Value - y[0].Value) / y[0].Value
        })                                      //[F, +100%]; [S, -20%]
    );

using (IDisposable subscription = differential.Subscribe(trader))
{
    Observable.Wait(market);
}

发生以下三个错误之一:

  • 以下ArgumentExceptionReactive Extensions 中抛出

      

    System.ArgumentException:已添加具有相同键的项。      在System.ThrowHelper.ThrowArgumentException(ExceptionResource资源)      在System.Collections.Generic.Dictionary`2.Insert(TKey键,TValue值,布尔加法)      在System.Reactive.Linq.Observable.GroupBy&#39; 3 ._。OnNext(TSource值)

  • 以下IndexOutOfRangeException

      

    参数名称:index      在System.ThrowHelper.ThrowArgumentOutOfRangeException(ExceptionArgument参数,ExceptionResource资源)      在System.Collections.Generic.List&#39; 1.get_Item(Int32 index)      在StockMarketTests。&lt;&gt; c__DisplayClass0_0.b__2(IList&#39; 1年)      在System.Reactive.Linq.Observable.Select&#39; 2 ._。OnNext(TSource值)

  • 偶尔Console调整的文字(颜色应该是一致的):

Console

什么可能导致这些古怪的症状?

2 个答案:

答案 0 :(得分:6)

关于Reactive Extensions概念的最大好处之一是能够订阅'发生'IObservable'somewhere'< / em>并在此'occurrence'上应用面向对象的概念 - 这不必知道'某处'的位置。

这种方式Reactive Extensions简化了event面向编程和producer-consumer problems很多

  

Great Power

在不知道观察数据来源的情况下订阅IObservable的能力会强制订阅者认为 通知是不可预测的 。换句话说,在观察IObservable 时,您应该假设通知可以同时发送

由于Reactive Externsions的行为合约,IObservables应该一次生成一个项目。通常,这就是发生的事情,但有时外部实施不遵循该合同。

让我们来看看这三个问题中的每一个:

GroupBy不是线程安全的

GroupBy的工作方式是返回IObservable<IGroupedObservable<T>>,其OnNext方法调用外IObservableOnNextIGroupedObservable<T>IGroupedObservable<T>匹配当前通知。它是通过为Subject<T>中的每个密钥保留一个Dictionary (更确切地说是一个ConcurrentDictionary来实现的 - 这不足为奇 - not Buffer(2, 1) 。这意味着 两个邻近通知可能导致双重插入

Select并非一个人

Select的线程安全性由其提供的委托决定。在上述情况中,Select方法提供的代理依赖于Buffer将提供大小为2的列表这一事实。 Queue包含{ {1}},这不是并发 ,因此当从多个线程迭代时 - Buffer的{​​{1}}可以为我们提供一些意想不到的结果< / EM>

如果Queue提供Exception ynull Queue可能会因同一原因而被抛出的StockTrader OnNext可以在迭代时修改它。

即使基本观察也不安全

最后但并非最不重要的是,即使您只进行基本观察,OnNext的{​​{1}}方法也会在非NullReferenceException中修改控制台,从而导致奇怪的文本布局

那你能做什么?

InvalidOperationException

存在atomic operation方法,使您能够验证您是否订阅了线性 Threads,这意味着 不超过一次调用IObservable<CompanyInfo> differential = market //[F, 1], [S, 5], [S, 4], [F, 2] .Synchronize() .GroupBy(x => x.Name) //[F, 1], [F, 2]; [S, 5], [S, 4] .SelectMany(x => x //4, 8, 2, 3 .Buffer(2, 1) //(4, 8), (8, 2), (2, 3), (3) .SkipLast(1) //(4, 8), (8, 2), (2, 3) .Select(y => new CompanyInfo //(+100%), (-75%), (+50%) { Name = x.Key, Value = (y[1].Value - y[0].Value) / y[0].Value }) ); //[F, +100%]; [S, -20%] 方法可以同时发生

由于即使Synchronize扩展方法也不是线程安全的,因此需要在链的开头调用IObservable<T>方法:

Synchronize

请注意,Observable会在您的查询中添加另一个代理SELECT * FROM news JOIN firm ON IDFIRM = firm.ID WHERE Block = 0 JOIN publisher ON id_publisher = publisher.ID WHERE Block = 0 ,因此会使查询慢一点,因此 您应该避免在不需要时使用它

答案 1 :(得分:1)

您的代码问题不在于查询,也不在于Rx本身。问题可能来自您实际的StockMarketStockTrader实施。

现在,问题很可能是由于您要为market观察点创建两个订阅而导致问题。

当你这样写:

using (IDisposable subscription = differential.Subscribe(trader))
{
    Observable.Wait(market);
}

...您正在获得market的两个订阅。一个位于differential.Subscribe(trader),另一个位于Observable.Wait(market);

我怀疑两个并发订阅会导致您的问题,但如果没有看到StockMarket的实施,我们无法说明它为什么会抛出。

这是实现您自己的可观察和观察者实现的危险。你应该避免这样做。最好使用标准Rx运算符构建的IObservable<CompanyInfo> CompanyValues { get; }属性CompanyInfo挂起。

您应始终避免阻止.Wait(...)等操作。

作为快速测试,我会将当前Observable.Wait(market);替换为具有足够长睡眠时间的Thread.Sleep(?),以查看您的代码是否有效。当然,您需要确保在后台计划程序上生成值(例如Scheduler.Default)。

我运行此代码来测试您的查询:

public class CompanyInfo
{
    public string Name { get; set; }

    public double Value { get; set; }
}

public class StockTrader : IObserver<CompanyInfo>
{
    public void OnCompleted()
    {
        Console.WriteLine("Market Closed");
    }

    public void OnError(Exception error)
    {
        Console.WriteLine(error);
    }

    public void OnNext(CompanyInfo value)
    {
        WriteStock(value);
    }

    private void WriteStock(CompanyInfo value) { Console.WriteLine($"{value.Name} = {value.Value}"); }
}

public class StockMarket : IObservable<CompanyInfo>
{
    private CompanyInfo[] _values = new CompanyInfo[]
    {
        new CompanyInfo() { Name = "F", Value = 1 },
        new CompanyInfo() { Name = "S", Value = 5 },
        new CompanyInfo() { Name = "S", Value = 4 },
        new CompanyInfo() { Name = "F", Value = 2 },
    };

    public IDisposable Subscribe(IObserver<CompanyInfo> observable)
    {
        return _values.ToObservable().ObserveOn(Scheduler.Default).Subscribe(observable);
    }
}

......用这个:

StockMarket market = new StockMarket();
StockTrader trader = new StockTrader();

IObservable<CompanyInfo> differential = market  //[F, 1], [S, 5], [S, 4], [F, 2]
    .GroupBy(x => x.Name)                       //[F, 1], [F, 2]; [S, 5], [S, 4]
    .SelectMany(x => x                  //4, 8, 2, 3
        .Buffer(2, 1)                   //(4, 8), (8, 2), (2, 3), (3)
        .SkipLast(1)                    //(4, 8), (8, 2), (2, 3)
        .Select(y => new CompanyInfo    //(+100%), (-75%), (+50%)
        {
            Name = x.Key,
            Value = (y[1].Value - y[0].Value) / y[0].Value
        })                                      //[F, +100%]; [S, -20%]
    );

IDisposable subscription = differential.Subscribe(trader);

Thread.Sleep(10000);

我没有一次崩溃或造成任何例外。