STM性能不佳/锁定

时间:2011-06-22 12:36:03

标签: performance haskell concurrency stm

我正在编写一个程序,其中有大量代理侦听事件并对其做出反应。由于Control.Concurrent.Chan.dupChan已被弃用,我决定使用TChan作为广告宣传。

TChan的表现比我预期的要糟糕得多。我有以下程序说明了这个问题:

{-# LANGUAGE BangPatterns #-}

module Main where

import Control.Concurrent.STM
import Control.Concurrent
import System.Random(randomRIO)
import Control.Monad(forever, when)

allCoords :: [(Int,Int)]
allCoords = [(x,y) | x <- [0..99], y <- [0..99]]

randomCoords :: IO (Int,Int)
randomCoords = do
  x <- randomRIO (0,99)
  y <- randomRIO (0,99)
  return (x,y)

main = do
  chan <- newTChanIO :: IO (TChan ((Int,Int),Int))

  let watcher p = do
         chan' <- atomically $ dupTChan chan
         forkIO $ forever $ do
                    r@(p',_counter) <- atomically $ readTChan chan'
                    when (p == p') (print r)
         return ()

  mapM_ watcher allCoords

  let go !cnt = do
       xy <- randomCoords
       atomically $ writeTChan chan (xy,cnt)
       go (cnt+1)

  go 1

编译(-O)并运行程序时,首先会输出如下内容:

./tchantest
((0,25),341)
((0,33),523)
((0,33),654)
((0,35),196)
((0,48),181)
((0,48),446)
((1,15),676)
((1,50),260)
((1,78),561)
((2,30),622)
((2,38),383)
((2,41),365)
((2,50),596)
((2,57),194)
((3,19),259)
((3,27),344)
((3,33),65)
((3,37),124)
((3,49),109)
((3,72),91)
((3,87),637)
((3,96),14)
((4,0),34)
((4,17),390)
((4,73),381)
((4,74),217)
((4,78),150)
((5,7),476)
((5,27),207)
((5,47),197)
((5,49),543)
((5,53),641)
((5,58),175)
((5,70),497)
((5,88),421)
((5,89),617)
((6,0),15)
((6,4),322)
((6,16),661)
((6,18),405)
((6,30),526)
((6,50),183)
((6,61),528)
((7,0),74)
((7,28),479)
((7,66),418)
((7,72),318)
((7,79),101)
((7,84),462)
((7,98),669)
((8,5),126)
((8,64),113)
((8,77),154)
((8,83),265)
((9,4),253)
((9,26),220)
((9,41),255)
((9,63),51)
((9,64),229)
((9,73),621)
((9,76),384)
((9,92),569)
...

然后,在某些时候,将停止写任何东西,同时仍然消耗100%的CPU。

((20,56),186)
((20,58),558)
((20,68),277)
((20,76),102)
((21,5),396)
((21,7),84)

使用-threaded,锁定速度更快,仅在少数几行之后发生。它还将消耗通过RTS'-N标志提供的任何数量的内核。

此外,性能似乎相当差 - 每秒只处理大约100个事件。

这是STM中的错误还是我误解了STM的语义?

3 个答案:

答案 0 :(得分:21)

该计划的表现非常糟糕。你将产生10,000个线程,所有这些线程都会排队等待单个TVar被写入。所以一旦他们全力以赴,你很可能会发生这种情况:

  1. 10,000个线程中的每个线程都尝试从通道读取,发现它为空,并将自身添加到底层TVar的等待队列中。因此,您将在TVar的等待队列中拥有10,000个队列事件和10,000个进程。
  2. 有些东西被写入频道。这将使10,000个线程中的每一个出错,并将其放回运行队列(这可能是O(N)或O(1),具体取决于RTS的写入方式)。
  3. 然后,10,000个线程中的每个线程都必须处理该项目以查看它是否对它感兴趣,而大多数线程都不会对它感兴趣。
  4. 因此每个项目将导致处理O(10,000)。如果您每秒看到100个事件,则意味着每个线程需要大约1微秒才能唤醒,读取几个TV,写入一个并再次排队。这似乎并不那么无理。我不明白为什么程序会彻底停止。

    一般情况下,我会废弃此设计并将其替换为:

    有一个线程读取事件通道,该通道维护从坐标到感兴趣的接收器通道的映射。然后单线程可以在O(log N)时间内从地图中选出接收器(比O(N)好得多,并且涉及的常数因子要小得多),并将事件发送给感兴趣的接收器。因此,您只需向相关方执行一两次通信,而不是与所有人进行10,000次通信。基于列表的构思形式在本文第5.4节的CHP中写成:http://chplib.files.wordpress.com/2011/05/chp.pdf

答案 1 :(得分:9)

这是一个很棒的测试案例!我认为你实际上创造了一个罕见的真正活锁/饥饿的例子。我们可以通过使用-eventlog进行编译并使用-vst进行编译,或使用-debug进行编译并使用-Ds进行运行来对此进行测试。我们看到即使程序“挂起”,运行时仍然像疯了一样,在被阻塞的线程之间跳转。

高级别的原因是你有一个(快速)作家和许多(快速)读者。读者和作者都需要访问表示队列末尾的相同tvar。假设一个线程成功,并且在发生这种情况时所有其他线程都失败了。现在,当我们将争用中的线程数增加到100 * 100时,读者快速进展的概率趋向于零。与此同时,作者实际上在访问该tvar时需要更长而不是读者,因此这会使事情变得更糟。

在这种情况下,在每次调用go之间为作者(例如threadDelay 100)设置一个小节流就足以解决问题。它为读者提供了足够的时间来连续写入之间的所有阻塞,因此消除了活锁。但是,我认为改善运行时调度程序的行为以处理这种情况将是一个有趣的问题。

答案 2 :(得分:6)

除了Neil所说的,你的代码也有空间泄漏(较小n明显):Space leak在修复明显的元组构建问题by making tuples strict之后,我离开了使用以下配置文件:Profile with strict tuples我认为,这里发生的主要问题是主线程将数据写入共享TChan的速度比工作线程可以读取的速度快TChan,如{ {1}},无限制)。所以工作线程spend most of their time重新执行它们各自的STM事务,而主线程忙于将更多数据填充到通道中;这就解释了为什么你的程序会挂起。