【问题标题】:Ensuring Thread Safety确保线程安全
【发布时间】:2010-03-04 05:55:48
【问题描述】:

我正在编写一个 C# Windows 窗体应用程序,该应用程序通过算法(策略)处理来自市场的报价,从而为经纪公司创建订单。这一切似乎都测试得相当好,直到我尝试建立在其自己的线程上同时运行多个策略的能力。此时一切都开始运行不正确。我相信我有一些不是线程安全的类,它们会导致不稳定的行为。任何关于我如何以线程安全的方式线程化的见解都非常感谢!

报价输入算法的方式如下: 1) 市场数据事件从 Brokers Software 触发到我的软件中名为 ConnectionStatus 的客户端类。当触发市场数据事件时,会根据这些静态变量的当前值构建一个 Quote 对象,这些静态变量代表 Bid、ask 等。 一旦报价建立,我会努力将其发送到每个正在运行的策略算法中。这是我用来执行此操作的代码:

 foreach (StrategyAssembler assembler in StrategyAssembleList.GetStrategies())
 {                  
     BackgroundWorker thread = strategyThreadPool.GetFreeThread();
     if (thread != null)
     {
        thread.DoWork += new DoWorkEventHandler(assembler.NewIncomingQuote);
        thread.RunWorkerAsync(quote);
     }   
 }

StrategyAssembler 是一个创建类 StrategyManager 实例的类,该类又创建一个包含实际算法的策略实例。可能有 4 或 6 个不同的 StrategyAssembler 实例,每个实例都被添加到 StrategyAssembleList 的 Singleton 实例中,该实例是一个 BindingList。

传入的报价对象被传递到 StrategyAssembler 类的 NewIncomingQuote 方法中。该代码如下:

public void NewIncomingQuote(object sender, DoWorkEventArgs e)
    {
        Quote QUOTE = e.Argument as Quote;            

        lock (QuoteLocker)
        {
            manager.LiveQuote(QUOTE);

            priorQuote = QUOTE;
        }
    }

我在想,通过在将报价传递给 manager.LiveQuote(Quote quote) 方法之前使用锁,所有使用报价“下游”的对象都可以在线程安全中使用报价时尚,但测试显示并非如此。有没有一种方法可以将 StrategyAssembler 的每个实例放在它自己的线程上,以确保 Strategy Assembler 创建的所有对象都是线程安全的,然后将引用提供给 StrategyAssembler?这种思维方式是否适合处理这种情况?

提前感谢您的任何反馈或帮助,

学习1

【问题讨论】:

  • 1) manager.LiveQuote() 做什么?它是您的策略方法的接口吗? 2) QuoteLockerStrategyAssembler 实例成员吗? 3) 最后,你期待什么样的行为,你看到了什么?
  • 1) manager.Livequote() 是 a) 将报价添加到由指标类和策略算法类和 OrderFill 模拟类引用的列表中 b) 将报价传递到 StrategyAlgorithm 的方法c)管理Strategy中的订单对象,模拟订单填写/取消/通信延迟等。
  • 2) QuoteLocker 是一个静态对象 static object QuoteLocker = new object();
  • 3) 在行为方面,我希望每个报价在到达时被添加到 StrategyAssembler 的每个实例中,并从那里添加到 manager.LiveQuote。从那里,该策略通过其算法来决定买入/卖出/什么也不做。每个 Assembler 实例只有 1 个 StrategyManager 实例,每个 StrategyManager 实例只有 1 个策略类,用于传递报价和管理订单。我看到的行为是将单引号添加到给定的汇编器实例 1 或 2 或 3 或 4 次,而根本没有添加到不同的汇编器实例
  • 这些建议对你有用吗?

标签: c# multithreading threadpool


【解决方案1】:

锁定应该在读取和写入任何共享状态时发生。没有读锁,代码仍然可以并发读写。

您可以将读写锁定包装到管理器中。

【讨论】:

  • 感谢消费者的反馈。你是说在读取引用对象的代码中的每个“下游”点我都需要一个锁吗? (下游有很多地方和对象使用每个引用的值)。
  • 将锁包裹到管理器中是一种危险的方式。例子。如果您有(锁定)读取和(锁定)写入,这感觉很安全。但是,如果您执行锁定的读取()、递增、锁定的写入(),您仍然会违反您的关键部分。锁定应该围绕数据的总使用量,从而对数据进行原子修改。
  • @spender 在这种情况下,最好复制报价并避免锁定期...事实上,适当的锁定将使他的策略按顺序运行,这将使多线程工作完全无用等等由于锁定 + 上下文切换的开销,计算成本很高。
【解决方案2】:

如果:

1) 策略通过LiveQuote 方法调用,可以修改Quote 实例。

2) 对 Quote 实例的更改不应在策略之间共享。

您需要在调用LiveQuote() 之前创建提供的Quote 的副本并将副本发送到策略方法而不是原始报价。根据其他要求,您可能根本不需要任何锁定。

【讨论】:

    【解决方案3】:

    您的代码中发生了两件事:
    1. 您从一个线程(生产者又名市场数据馈送)收到报价。
    2. 您将报价发送到另一个线程(消费者 AKA StrategyAssembler)。

    此时报价存在争用,换句话说,生产者线程和每个消费者线程(即策略的每个实例)都可以修改您刚刚提供的报价。为了消除争用,您必须做以下三件事之一:

    1. 在所有有权访问报价的线程之间同步。
      或者
    2. 使引用不可变(并确保生产者不会替换它)。
      或者
    3. 给每位消费者一份报价单。

    对于您的情况,我建议您采用第三个选项,因为锁定比复制报价更昂贵(我希望您的报价不是很大)...选项二也不错,但您的策略不应该修改报价.

    通过向每位消费者提供报价副本,您可以确保他们不会共享任何数据,因此没有其他线程会修改报价,您将消除争用。如果您的策略没有创建任何其他线程,那么您就完成了。

    一般而言,您应该避免锁定,并且应该尽量减少数据共享,但是如果您必须在线程之间共享数据,那么您应该正确地进行:
    为了使您的策略正确同步,它们必须在同一个QuoteLocker 对象上同步,即QuoteLocker 必须对每个线程可见。即使您做得正确并且使您的策略同步(锁定QuoteLocker),那么您也可能没有线程......您将运行上下文切换+锁定的开销,并且您的策略将按顺序执行相同的报价。

    按 cmets 更新: 如果您将代码保持原样(意味着您为每个线程提供报价的副本),那么我不明白为什么您的其他策略在第一个策略完成之前不会获得报价......您的第一个策略将最可能在创建其他策略的线程时开始工作。让你的策略在一个单独的线程中运行的全部意义在于避免这个问题……你启动一个新线程,这样你的其他策略就不会互相等待完成。

    这部分代码很可能会在所有线程开始工作之前完成...

    foreach (StrategyAssembler assembler in StrategyAssembleList.GetStrategies())
     {                  
         BackgroundWorker thread = strategyThreadPool.GetFreeThread();
         if (thread != null)
         {
            thread.DoWork += new DoWorkEventHandler(assembler.NewIncomingQuote);
            Quote copy = CopyTheQuote(quote);// make an exact copy of the quote
            thread.RunWorkerAsync(copy);
         }   
     }
    

    在您创建话题时,您的市场信息是否会改变实际报价?市场提要通常会提供快照,因此除非在您制作线程时有什么东西改变了您的报价,否则上面的设计应该没问题。如果设计有问题,那么我可以为您提供基于阻塞队列的生产者和多个消费者设计,这也非常有效(you can check out this discussion for an idea on how it works,我可以告诉您如何针对您的具体示例进行修改)。

    【讨论】:

    • 是的,预防胜于修复。如果您的线程可以独立运行(选项 3),您将拥有最安全和性能最佳的选项。由于您无法保证首先完全处理第一个报价,因此您可能需要在副本中添加(高分辨率)时间戳。
    • 感谢 Lirik 的反馈。这一切都很有道理。由于您提到的原因以及其他原因,选项对我来说最有意义。在制作副本并将其传递给汇编程序的每个实例方面,我认为这最好通过一个事件来完成,这样所有汇编程序都可以同时获取他们的报价副本,然后可以处理他们的逻辑。否则,如果它留在循环中,以下策略将不会得到报价,直到第一个完成其逻辑(大约 100 毫秒,报价可以在 50 毫秒内到达)。
    • 如果我将引用放在每个汇编程序订阅的事件中,那么让每个汇编程序在线程上运行以便我可以利用 CPU 上的更多内核的最佳方法是什么?目前,这些汇编程序和各自创建的策略算法都运行在主线程上
    • 感谢 Lirik 的洞察力,我会试试这个版本,看看它如何与报价副本一起工作。在市场变化方面,上面的代码 sn-p 在 BuildQuote() 方法中。每次 BidPrice、BidVolume、AskPrice 或 AskVolume 发生变化时都会调用此方法。每次调用时,都会使用代理数据事件更改的静态变量创建一个新报价。我不知道一个生产者和多个消费者的设计是否会是一个更好的方式来做到这一点。我会看看你发布的链接 - 谢谢!
    • 您创建了一个新报价,但您要替换当前用于初始化线程的报价吗?
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2014-10-26
    • 1970-01-01
    相关资源
    最近更新 更多