Warning: file_get_contents(/data/phpspider/zhask/data//catemap/6/multithreading/4.json): failed to open stream: No such file or directory in /data/phpspider/zhask/libs/function.php on line 167

Warning: Invalid argument supplied for foreach() in /data/phpspider/zhask/libs/tag.function.php on line 1116

Notice: Undefined index: in /data/phpspider/zhask/libs/function.php on line 180

Warning: array_chunk() expects parameter 1 to be array, null given in /data/phpspider/zhask/libs/function.php on line 181
C#跨线程安全使用LINQ_C#_Multithreading_Linq_Thread Safety - Fatal编程技术网

C#跨线程安全使用LINQ

C#跨线程安全使用LINQ,c#,multithreading,linq,thread-safety,C#,Multithreading,Linq,Thread Safety,我有一个程序,它不断地从WebSocket读取和解析大量数据流。所有解析都发生在客户机内的一个线程上,数据被组织成一个SortedSet树,以便快速操作 所有数据的添加、更新和删除都不会出现任何问题 当我试图从另一个线程访问数据时,问题就出现了。它会跑得很好,但在赛道的某个地方,一到两分钟内就会达到比赛状态 考虑以下代码(在自己的线程上运行)以近乎实时地更新UI: private async Task RenderOrderBook() { var book = _client.Orde

我有一个程序,它不断地从WebSocket读取和解析大量数据流。所有解析都发生在客户机内的一个线程上,数据被组织成一个
SortedSet
树,以便快速操作

所有数据的添加、更新和删除都不会出现任何问题

当我试图从另一个线程访问数据时,问题就出现了。它会跑得很好,但在赛道的某个地方,一到两分钟内就会达到比赛状态

考虑以下代码(在自己的线程上运行)以近乎实时地更新UI:

private async Task RenderOrderBook()
{
    var book = _client.OrderBook;

    while (true)
    {
        try
        {
            var asks = book.Asks.OrderBy(i => i.Price).Take(5).OrderByDescending(i => i.Price);
            var bids = book.Bids.OrderByDescending(i => i.Price).Take(5);

            orderBookView.BeginInvoke(new MethodInvoker(() =>
            {
                ...omitted due to irrelevance
            }));

            await Task.Delay(500);
        }
        catch (Exception ex)
        {
            ex.ToString();
        }
    }
}
竞态条件位于
book
上的LINQ操作中。常见的错误是
i.Price
(一个
decimal
变量),或者可能只是
i
所指的对象是空的。此外,我试图吞下异常的拙劣尝试实际上是行不通的

无论如何,我的猜测是,数据被解析和操作的速度如此之快,以至于最终在使用LINQ OrderBy操作时,它会遇到一种情况,即客户端删除了一个节点,尝试从中读取数据,并引发异常

book.Asks
book.Bids
属性最初的类型是
SortedSet
,直接指向数据成员本身。为了缓解这种竞争条件场景,我尝试将它们更改为节点数组,并使用
\u asks.ToArray()
调用创建一个副本以供读取。这有助于减少问题发生的频率,但它仍然会发生

如何使此线程安全

其他代码片段

public PriceNode[] Asks
{
    get { return _asks.ToArray(); }
}

public PriceNode[] Bids
{
    get { return _bids.ToArray(); }
}

我的UI开发的第一条规则是,永远不要在UI线程上执行I/O。听起来你已经搞定了

我的第二条规则是,一旦某个东西对UI线程可见,就不能从任何其他线程接触它。这个规则有一个例外,那就是不可变数据:如果一个对象不会改变,那么任何线程都可以接触它。可变数据禁止触摸。请记住,“可变数据”包括大多数集合

如果你能遵守这两条规则,你的生活会轻松得多。跟随其中一个而不打破另一个可能很棘手,但有办法做到这一点,一旦你有一个像样的把握,你会在一个更好的地方。启蒙之路从这里开始:

您的读取线程(从套接字读取的线程)可以创建它想要的所有新对象,但不能更新现有对象。它也不能修改UI线程正在使用的任何集合。如果您只是添加新对象,这并不糟糕:您的读取线程可以从套接字中提取数据,并使用它来创建新对象。当这些对象准备好后,它必须将它们交给UI线程,UI线程可以将它们添加到相关集合中。根据Strobel规则1,大部分工作(以及所有I/O)发生在读取线程上,这正是我们想要的。相比之下,“提交”已经填充的对象的行为应该是微不足道的。根据规则#2,一旦任何可变对象被交给UI线程,您的读取线程就不能再接触它们。永远

更新现有对象更为棘手。有几种方法可以解决这个问题。一种是让读取线程使用最新数据创建新对象,然后将其交给UI线程。如果您有非常简单的对象图,最简单的选择可能是简单地用新版本替换旧对象,记住任何引用旧对象的UI代码都需要知道它已被替换。或者,UI线程可以使用来自新对象的数据来更新现有对象。如果您遵循规则#2,这将是完全线程安全的,并且任何指向旧对象的UI代码都会自动看到新数据,而不会出现任何读取错误或其他与种族相关的污点。这种方法可能是你最好的选择

如果在尝试了上一段中的方法之后,您发现您正在生成不可接受的垃圾量,那么还有第三种选择。读取线程可以将每个对象的原始数据复制到临时缓冲区中,然后将缓冲区交给UI线程,UI线程可以使用缓冲区中的数据更新现有对象。这意味着在UI线程上要做更多的工作,但至少数据已经在内存中(套接字I/O已经完成)。由于这种方法的要点是创建更少的垃圾,因此只有重用缓冲区才有意义。这意味着您需要一个线程安全的缓冲池。读取线程获取一个临时缓冲区,从套接字填充它,将其交给UI线程,UI线程完成后将其返回到池中。精明的读者会注意到,在线程之间传递可变缓冲区会违反规则#2,因此请注意,一旦线程交付缓冲区,它就会立即忘记它。因为这种方法需要对线程安全性有更深入的了解,才能使池正常工作,所以我建议将其作为最后的手段。如果你能逃脱上一段中的一个选项,请这样做

无论使用哪种方法更新现有对象,都需要一种将新对象/数据与旧对象匹配的方法。如果每个对象都有唯一的标识符,则可以使用
字典
作为有效的查找机制。用新的副本替换旧对象要复杂一些,因为旧版本可能分散在多个集合中,其中一些可能不支持高效替换

最后一件事:当您将新的/更新的对象移交给UI线程时,最好是分批执行。例如,您最好向UI线程发布一个操作来更新100个对象,而不是发布100个单独的对象