Edited at

C#でマルチスレッドのベストプラクティスって何かある?(What are the best practices with multithreading in C#?)

More than 1 year has passed since last update.

StackExchange/Code Reviewでの質問"Exporting doc types using queues and multithreading"へのEric Lippert氏による回答より訳出。回答内容はオリジナル投稿"What are the best practices with multithreading in C#?"に呼応するため、編集前のタイトルを採用。原文および訳文のライセンスは引用元サイト規約の通り CC-BY-SA 3.0 に従う。

(補足:回答内容のトーンに合わせて口語調かつ意訳気味に訳出しました。誤訳指摘および訳出改善は歓迎します。)


あんたのそのコード -- ひと目見ただけで酷いバグだらけ -- をレビューするよりも、質問に直接答えていこうか:


C#でマルチスレッドのベストプラクティスって何かある?



  1. そんなものは無い。マルチスレッドは悪い考えだ。プログラム中に 1つ の制御フローがあるだけでも理解するのが十分難しいってのに;一体どうして2つ目、3つ目、4つ目を増やそうとするんだ?途方も無く再現性が低くて、超難解なバグを見つけだす作業を楽しみたい?あんたは弱いメモリモデルと強いメモリモデルの違いについて十分な知識があるか?ない?じゃあマルチスレッドプログラムを書くのなんて止めとけ!プロセスは申し分のない処理単位だ;そういうことをしたければ、プロセスを生成するんだね。


  2. ああ、とにかくマルチスレッドプログラミングをやりたいんだな。その場合は提供されるなかで最もハイレベルの抽象を使え。スレッドを直接扱うプログラミングは、ジョブ(job) に関するプログラムを書くのではなく、ワーカ(worker) に関するプログラムを書くことを意味する。思い出してほしい、ワーカは結末に過ぎない;あんたが本当に処理したいのはジョブのはずだ。Task Parallel Libraryを使え:タスクが ジョブ(job) で、スレッドが ワーカ(worker) だ。TPLはワーカの効率的な使い方を教えてくれるだろう。


  3. 待て、1つ目の指摘に立ち返ってみよう。本当に並行性が必要なのか?と自問してくれ。真の並行性では、プログラム中に 同時刻において実際にアクティブな 2つ以上の制御ポイントを持つことになる。疑似的な並行性ではプロセッサにより2つの制御ポイントが交互に実行されるため、それらは実際の並行処理ではない。プロセッサ数よりも多数のスレッドが存在するため、ほとんどの場合は疑似的な並行性に終わる。このことは、我々に必要なのは実際の並行性ではなかったのではという結論を導く。大抵の場合、人々が真に求めるのは 非同期性(asynchrony) だ;並行性は非同期性を達成する一つの手段だが、唯一のものではない。非同期性のワークフローを表現したいなら、新しくC# 5.0で導入されたasync/await機能を使え


  4. あんたが前の指摘に混乱して、しばらく考えても、プロセス内で複数の制御スレッドなしに非同期性を達成する手段はないと考えるなら、Stephen Clearの記事スレッドは存在しない(There Is No Thread)を読め。記事を理解するまではマルチスレッドプログラムを書こうとするな。


  5. 是が非でもメモリの共有を避けろ。大半のスレッド関連バグは、現実世界での共有メモリセマンティクスに対する誤った理解から生じる。スレッド生成が必須だというなら、あたかもプロセスのように扱うんだ:あるスレッドの処理で必要となる全てを与え、他のスレッドに関連するメモリは変更 せずに 処理を実行しろ。あるプロセスが他プロセスのメモリを変更できないのと同じことだ。


  6. あー、信じられない程バグの温床になるとしても、あんたはスレッド間でメモリを共有したいんだな?繰り返しになるが、利用可能な最高レベルのツールを使うことを立ち返ってほしい。それらのツールは専門家により書かれている。あんたは専門家じゃない。遅延評価される値をフィールドへ書き出すとき、遅延評価が1回以上実行されずに、かつ任意のプロセッサキャッシュ状態においても一貫性のない状態に置かれたフィールドを決して観測しないことを 絶対的に保証 する方法を、あんたは知っているのか?俺は知らない。おそらくあんたもだ。Joe Duffyは知っているし、それが自前コードじゃなくて彼が書いたLazy<T>プリミティブを使うべき理由だ。


  7. あぁ、それでもあんたはメモリを共有するコードを書きたいんだな?なら一生のお願いだからロックを取ってくれ。俺はロックが高価だと信じ込む連中によるバグだらけのコードを見てきたし、奴らはたった12ナノ秒を要するロック獲得コストを回避するために、入り組んで悲惨に間違ったコードを書くんだ。コスト抑制を理由にロックを避けることは、スイッチを入れたとき電球まで電気が届くのが遅すぎると不平を漏らすようなものだ。ロックはメモリ一貫性を保証する強力なツールだ;スレッドレベルでメモリを共有するプログラムをするなら、危険を覚悟でロックを避けることだね。


  8. プロダクションコードでThread.Sleepを引数0や1以外で使っているとしたら、それは恐らく間違ったことをしている。スレッドは高価だ;ワーカを眠らせるべきでないし、スレッドも眠らせるべきでない。タイミング問題を回避して正しい動作のためにsleepしている -- まさにコードがやっているように -- としたら、あんたは確実に深刻な間違いを犯している。タイミングに依らずマルチスレッドコードは正しく動作する必要がある。