次の条件が満たされている場合、この設計はおそらく問題ありません。
- 読み取りは書き込みよりもはるかに一般的です
- 多数の読み取りが書き込みの間に散在します
- (おそらく) 書き込みは、グローバル データ構造のごく一部にのみ影響します。
もちろん、これらの条件を考えると、ほぼすべての同時実行システムで問題ありません。あなたはライブロックを懸念しているので、より複雑なアクセス パターンを扱っているのではないかと思います。その場合は、読み進めてください。
あなたのデザインは、次の一連の推論によって導かれているようです。
atomicModifyIORef
サンクを作成するだけなので、非常に安価です
安価なためatomicModifyIORef
、スレッドの競合が発生しません
安価なデータ アクセス + 競合なし = 同時実行性 FTW!
この推論に欠けているステップは次のとおりです。IORef
変更はサンクを作成するだけであり、サンクが評価される場所を制御することはできません。データが評価される場所を制御できない場合、真の並列性はありません。
意図したデータ アクセス パターンをまだ提示していないため、これは憶測ですが、データへの変更を繰り返すと、一連のサンクが構築されることが予想されます。その後、ある時点でデータを読み取って評価を強制し、それらのすべてのサンクが 1 つのスレッドで順番に評価されるようにします。この時点で、最初からシングルスレッド コードを作成している可能性があります。
これを回避する方法は、データが IORef に書き込まれる前に (少なくとも希望する範囲で) 評価されるようにすることです。これが の戻りパラメータのatomicModifyIORef
目的です。
変更することを意図したこれらの関数を検討してくださいaVar :: IORef [Int]
doubleList1 :: [Int] -> ([Int],())
doubleList1 xs = (map (*2) xs, ())
doubleList2 :: [Int] -> ([Int], [Int])
doubleList2 xs = let ys = map (*2) xs in (ys,ys)
doubleList3 :: [Int] -> ([Int], Int)
doubleList3 xs = let ys = map (*2) xs in (ys, sum ys)
これらの関数を引数として使用すると、次のようになります。
!() <- atomicModifyIORef aVar doubleList1
- サンクのみが作成され、データは評価されません。次からどのスレッドを読んでも、不愉快な驚きですaVar
!
!oList <- atomicModifyIORef aVar doubleList2
(:)
- 新しいリストは、初期コンストラクター、つまりorを決定する場合にのみ評価され[]
ます。まだ実際の作業は行われていません。
!oSum <- atomicModifyIORef aVar doubleList3
- リストの合計を評価することにより、計算が完全に評価されることが保証されます。
最初の 2 つのケースでは、ほとんど作業が行われていないため、atomicModifyIORef
はすぐに終了します。 しかし、その作業はそのスレッドでは行われておらず、いつ行われるかはわかりません。
3 番目のケースでは、目的のスレッドで作業が行われたことがわかります。最初にサンクが作成され、IORef が更新されます。次に、スレッドが合計の評価を開始し、最終的に結果を返します。しかし、合計の計算中に他のスレッドがデータを読み取るとします。サンク自体の評価を開始する可能性があり、2 つのスレッドが重複した作業を行っていることになります。
一言で言えば、このデザインは何も解決していません。同時実行性の問題が難しくない状況では機能する可能性がありますが、検討しているような極端なケースでは、複数のスレッドが重複した作業を行ってサイクルを焼き尽くします。また、STM とは異なり、いつ、どのように再試行するかを制御することはできません。少なくとも STM では、トランザクションの途中でアボートすることができます。