C#非同期処理関連のMSDNの資料読んでみた(1)

MSDNで紹介されている.NET Framework開発ガイドの”並列処理と同時実行”を読んで
まとめてみます。

きっかけ

よくある話だけど、マルチスレッドでメインスレッドのコントロールにアクセスしたいってのが
一番のはじまりです。Delegate+Invokeとか、event、Task, Actionとかで色々やっていくうちに
色々ありすぎて、『どれがどれだっけ・・・』って頻繁に忘れるようになって、また調べて、を
繰り返しすようになっていたので、頭の整理のためにブログでまとめておこうと思ったのがきっかけです。

内容はMSDN .NET Frameworkの開発ガイド
内容を僕が読んだ範囲でまとめたものです。

めちゃくちゃ詳細まで書かれている反面、仕方ないことですが、
触りを知りたいという人にとって、やはりわかりづらい部分も多くあるように
感じました。(機械翻訳っていうのも影響してると思う)

僕も、効率的な、または慣用的なマルチスレッドプログラミングの記述方法を
サラッと知りたいなー、と思って調べ始めましたが、詳細な資料が
あったので、せっかくならと、時間をかけて読んでみることにしました。

MSDN読んだけど何言ってるかわかんない!っていう方に
少しでも概要伝われば、と思います。

この記事の内容はこちらに更に詳細に記載されています。

今回はマルチスレッドの触りだけです。

マルチスレッド利用のメリットとデメリット

メリット

  1. 時間がかかる処理を他の処理と切り離しバックグラウンドで行うことができる。(処理の両立)
  2. ユーザへの応答とデータ処理を同時に行える。(1と同じようなものですが)
  3. 処理に優先順位をつけることができる。

デメリット

リソース要件や競合の発生の可能性を考慮する必要が出てくること

リソース要件とは (MSDNより引用)
* システムは、プロセス、AppDomain オブジェクト、およびスレッドが必要とするコンテキスト情報を格納するためにメモリを消費します。 そのため、使用できるメモリの量によって、作成できるプロセス、AppDomain オブジェクト、およびスレッドの数は制限されます。

  • 多数のスレッドを追跡するには、プロセッサ時間を大量に消費します。 スレッドが多すぎると、ほとんどのスレッドで処理に重大な影響がでます。 現在のスレッドのほとんどが 1 つのプロセスにある場合は、ほかのプロセスにあるスレッドのスケジュール頻度が低下します。
  • 多数のスレッドを使用したコードの実行は制御が複雑なため、多くのバグの原因となる可能性があります。

  • スレッドを破棄するには、発生する可能性がある問題を認識して対処する必要があります。

つまりは

  1. スレッドが多すぎると処理が重くなる(スレッドのコンテキスト情報の格納する大量のメモリ、スレッド追跡するためのプロセッサ時間などに影響)
  2. 制御が複雑になるため、ソースコードが難解になり易い。結果バグが増える要因になる。
  3. スレッドの破棄にも時間を要する。

ということらしいです。

次に競合に関して

同期を必要とするリソース
* システム リソース (通信ポートなど)

  • 複数のプロセスによって共有されるリソース (ファイル ハンドルなど)
  • 複数のスレッドによってアクセスされる単一のアプリケーション ドメインのリソース (グローバル フィールド、静的フィールド、インスタンス フィールドなど)

  • これらのリソースに複数のスレッドからアクセスがあると、いわゆる競合が発生することがあります。
    これらに関して考慮し、適切に同期が行われないと、問題が発生することがある。ということです。

    問題というのは、『アプリケーションが動作しない』というだけでなく、正しく動作しているように見えても
    ファイルや変数の値などがおかしくなっていることもあるので注意が必要です。

    マルチスレッド処理におけるアプリケーションデザイン

    他のスレッドに影響を与えない、比較的短い処理を実行順序や時間を気にせず、とにかくマルチスレッドで実行
    させたい場合はThreadPoolクラスを利用するのが一番簡単(後述するけど推奨されているのは別の手法だし、
    僕個人は簡単と感じなかった)

    以下のケースに当てはまる場合、独自でスレッドを作成したほうがよい場合もある(以下そのまま引用)

    1. タスクに特定の優先順位を設定する必要がある場合。
  • タスクが長時間実行して、ほかのタスクをブロックする可能性がある場合。

  • スレッドをシングルスレッド アパートメント内に配置する必要がある場合 (すべての ThreadPool スレッドがマルチスレッド アパートメント内にある場合)。

  • スレッドに関連付けられた、確立された ID が必要な場合。 たとえば、そのスレッドをアボートまたは中断したり、名前で探索したりするには、専用のスレッドを使用する必要があります。

  • ユーザー インターフェイスと対話するバックグラウンド スレッドを実行する必要がある場合、.NET Framework Version 2.0 では、イベントを使用し、ユーザー インターフェイス スレッドへのスレッド間マーシャリングによって通信する BackgroundWorker コンポーネントを利用できます。

  • この辺は1と2はそのまんま。3はよくわかんない。4はThreadPoolだと、適宜使い回しされるから、
    ちゃんと区別したい場合、ってこと?5はそのまんま、UIからの入力と対話的に処理したいときはevent使ったり
    BackgroundWorkerを使ったら実現できますよ、ってことだと思います。

    んで、簡単にマルチスレッドができる、と言われているThreadPoolなんですが、
    じゃぁこれはどうやったらできるのか、というところを次に見て行きます。

    c#によるマルチスレッド。ThreadPoolを利用してみる。

    MSDNにあるサンプルソースコード
    を見て行きましょう。

    全然本編に関係ないんですが、なぜprivateのメンバーをクラス定義の
    末尾に書くんだろ、っていろんなサンプル見ていつも思ってます。
    (理由知ってる方教えてください><)

    ざっくりとソースコード読んでみましょう

    これがFibonacciクラスの概要

    main()メソッドを見ていきます。
    このサンプルを読みづらくしている要因(だと僕は思ってる)がManualResetEventクラス。

    これに関してはMDN ManualResetEventを見てもらえば書いてある通り、プログラマが自分の好きなように、イベント発生
    を通知できるクラス?だと思います。難しく考えず、ここでは

    ManualResetEventのインスタント全てがset()メソッド実行されるまで、メインスレッドの処理が
    待機している、と思ってもらえばOKです。

    大事なのは以下の部分

    10回ループして、内部でFibonacciクラスのインスタンスを生成し

    ThreadPool.QueueUserWorkItem(f.ThreadPoolCallback, i)を
    呼び出していいます。

    forループの外側では、

    WaitHandle.WaitAll(doneEvents);が実行され、doneEvents配列内部の
    ManualResetEventが、全部set()されるまで、処理が中断されます。

    つまり、 ThreadPool.QueueUserWorkItem(f.ThreadPoolCallback, i);
    この処理が別スレッドで処理されるようになっています。

    別スレッドの処理の状態をManualResetEventを利用して、メインスレッドで
    確認できるようにしています。

    MSDNのトピックに書かれているように、一番簡単、かはちょっとわかりませんが
    とりあえずこれが、ド定番、ThreadPoolクラスの使い方らしいです。

    実行結果は以下のようになります。

    ※ランダムで値を出しているので、手元で再現しても数字は変わります。

    スレッド処理と例外

    これ、意外と知られていない気がする

    スレッドで処理できない例外が発生すると、バックグラウンド
    スレッドの場合でも、通常、プロセスが終了します。 この規則には、次の 3 つの例外があります。

    1. Abort が呼び出されると、スレッドで ThreadAbortException がスローされる。
    2. アプリケーション ドメインがアンロードされるため、スレッドに AppDomainUnloadedException がスローされる。
    3. 共通言語ランタイムまたはホスト プロセスがスレッドを終了する。

    マルチスレッド処理の問題

    引用そのまま

    アプリケーションを終了せずに、スレッドが暗黙に失敗したまま放置されていると、プログラミングの深刻な問題が検出されない状態になる可能性があります。 長期間実行されるサービスや他のアプリケーションでは、これは特に問題となります。 スレッドが失敗すると、プログラムの状態が徐々に破損します。 アプリケーションのパフォーマンスが低下する場合もあれば、アプリケーションがハングする場合もあります。

    とのこと。つまり、マルチスレッドは便利だけど、扱いを間違えればアプリケーションが終了したり、
    ハングしたりすることがあります。
    それならまだ問題点に気が付きやすいけど、処理に失敗しているスレッドが放置され、アプリケーション
    が動き続けると、問題箇所が特定しづらいバグが発生する可能性があるから気をつけてね。とのこと。

    どのように、それらを上手に管理して開発していくべきなのか、ってところが一番知りたいんですが、
    ケースバイケースだ、ということと、いくつか資料があるのですが、長くなりそうなので別記事にします。

    この記事の参考リンクまとめ

    感想、とつづく…

    ThreadPoolのサンプルは、実用的だけどもっとも簡単なマルチスレッド実現手段として
    はどうなんだろ・・・wと少し思いました。リソースうんぬんを考慮しないで、とにかく
    マルチスレッド実現するなら、Threadクラスのほうが、わかりやすいかも・・・。

    まぁサンプルにあるManualResetEvent絡みがなければThreadPoolでももっともっと
    シンプルなサンプルは書けそうですね。

    あまりにも長くなりそうなので、記事いくつかに分けます。とりあえず
    今回はここまで。排他制御なんかもちゃんと取り上げたいです。

    次回はここを読んでいきたいです。

    ※追記

    資料読み進めて少しだけ思ったのが、いくら簡単だといっても、やっぱりとりあえず使うならThreadよりThreadPoolのほうが
    いいかもしんないです。理由とかはまとめられたら他の記事で。

    コメントを残す

    メールアドレスが公開されることはありません。 * が付いている欄は必須項目です