続・スレッドとオブジェクトインスタンス


さて、実は次のエントリは別の内容を書こうと思っていたのですが(現在書きかけ中)、質問がコメントで出ていたので、こちらを明確にしてから次の内容に進むことにします。(わからないことをそのままにしないのはめっちゃ重要ですからね~^^)

kazenami さんと HashedBeef さんのお二人のご質問に共通するポイントは、おそらく

  • スレッドはどのようにして開始されるのか?
  • オブジェクトインスタンスは、どのようにして生成され、どのようにして各スレッドのスタックメモリ内の変数に割り当てられるのか?

というところだと思います。前回のエントリでは、この部分をあやふやにして書いたために、きちんと考えようとすると、とたんに行き詰まりますよね^^。というわけで、もうちょっと補足しようと思います。

[3 種類のスレッド]

.NET アプリケーションの内部では、基本的に 3 種類のスレッドが動作しています。

  • メインスレッド
    そのプロセスに最初に作られ、Main() メソッドを動作するために使われるスレッド。
  • マニュアルスレッド
    バックグラウンドタスクを行うために、自力で作り起こすスレッド。System.Threading.Thread クラスのインスタンスを作ることで作成できる。
  • プールスレッド
    バックグラウンドタスクを行う際、CLR の機能であるスレッドプール機能を用いる際に利用されるスレッド。

これ以外にも、ファイナライザスレッドやアンマネージスレッドなどが動作しているのですが、とりあえずは上記 3 つを覚えれば十分。メインスレッドについては前回の説明でカバーされているので、ここではマニュアルスレッドとプールスレッドについて解説します。

[マニュアルスレッドについて]

まず、マニュアルスレッドから解説しましょう。マニュアルスレッドとは、Thread クラスのインスタンスを作ることで新規にスレッドを起こすというもので、下のコードのようにして作成することができます。細かいコードは覚える必要はありませんが、コード上、以下の点に注目してください。

  • マニュアルスレッドを新規作成する場合には、そのスレッドで何の処理をするのかを指定する必要がある。下記のコードの場合、Task() というメソッドを新規 Thread インスタンスの引数として渡すことにより、この Task() メソッドをマニュアルスレッド上で開始することができる。
  • スレッドには、フォアグラウンドスレッドとバックグラウンドスレッドの二種類があり、スレッドの IsBackground プロパティにより変更することができる。アプリケーションプロセス内のすべてのフォアグラウンドスレッドが終了すると、プロセスが終了する形となる。(=下のコードにおいて、IsBackground プロパティを false にして実行すると、Main() 関数が終了しても、プロセスは生き残り続けます。)
   1: namespace ConsoleApplication1
   2: {
   3:     class Program
   4:     {
   5:         static void Main(string[] args)
   6:         {
   7:             int a = 0;
   8:             int b = 0;
   9:             Thread t = new Thread(new ThreadStart(Task));
  10:             t.IsBackground = true;  // バックグラウンド化してから実行すると...
  11:             t.Start();
  12:             Console.ReadLine();
  13:         } // メインメソッド終了時(=すべてのフォアグラウンドスレッドが終了)に即座に終了する
  14:  
  15:         static void Task()
  16:         {
  17:             int i = 0;
  18:             while (true)
  19:             {
  20:                 i++;
  21:                 Thread.Sleep(500);
  22:                 Console.WriteLine("タスク実行中..." + i.ToString());
  23:             }
  24:         }
  25:     }
  26: }

image

このアプリケーションのメモリ配置は、下図のようになります。

※ 話を単純化するために、変数 t (当該スレッドの情報をラッピングしたオブジェクト)については記述を省略しています。今は、変数 a, b, i がどこに作られるのかに着目してください。

image

[スレッドとオブジェクトインスタンスのメモリ配置の関係]

では、このプログラムを少し改造して、クラスとオブジェクトインスタンスを扱うようなコードに変更してみます。(ちょっとコードがややこしくなっているので、よーく見てください)

   1: namespace ConsoleApplication1
   2: {
   3:     class Program
   4:     {
   5:         static X objX1 = new X();
   6:  
   7:         static void Main(string[] args)
   8:         {
   9:             int a = 0;
  10:             int b = 0;
  11:             X objX2 = new X();
  12:             Thread t = new Thread(new ThreadStart(Task));
  13:             t.IsBackground = true;
  14:             t.Start();
  15:  
  16:             objX1.Increment();
  17:             objX2.Increment();
  18:  
  19:             Console.ReadLine();
  20:         }
  21:         
  22:         static void Task()
  23:         {
  24:             int i = 0;
  25:             X objX3 = new X();
  26:             while (true)
  27:             {
  28:                 objX1.Increment();
  29:                 objX3.Increment();
  30:                 i++;
  31:                 Thread.Sleep(500);
  32:                 Console.WriteLine("タスク実行中..." + i.ToString());
  33:             }
  34:         }
  35:     }
  36:  
  37:     public class X
  38:     {
  39:         private int p = 0;
  40:         private string q = "Nobuyuki";
  41:  
  42:         public int Increment() { return p++; }
  43:     }
  44: }

この場合のメモリ配置は、以下のようになります。

image

さて、このコードの場合にはヤバいコードが存在します。それは、16 行目と 28 行目です。この 16 行目と 28 行目では、どちらも Program クラスの static データである objX1 を介して、&Hxxxx にある内部変数 p を操作する(インクリメントする)ことになります。二つのスレッドが同一変数を同時に操作する危険性があるため、このアプリケーションプログラムには、ロストアップデートの危険性がある、ということになるわけです。具体的には、下図のようなシチュエーションに陥ると、ロストアップデートの危険性があります。

image

このような問題を回避するために利用されるテクニックが、lock ブロックです。具体的には、以下のようなコードを記述します。

   1: public class X
   2: {
   3:     private int p = 0;
   4:     private string q = "Nobuyuki";
   5:  
   6:     public int Increment() 
   7:     {
   8:         lock (this)
   9:         {
  10:             return p++;
  11:         }
  12:     }
  13: }

このようにすると、複数スレッドが同時に同一インスタンス上で Increment メソッドを呼び出したとしても、8~11 行目を実行できるのは先に lock() 処理を行った方のみに限定されます。結果として、同時に二つのスレッドが変数 p を操作することはなくなり、ロストアップデートが発生することはなくなります。

image

# とまあ、マルチスレッドアプリケーションでは、うかつなコードを記述するとかなり危険なのです。><

[プールスレッドについて]

さて、引き続き、プールスレッドについて解説しましょう。一般的に、スレッドの作成/終了にはかなりのオーバヘッドがかかります。このため、上記のようなコードでマニュアルスレッドを作成するという処理は、一回だけ行うのならともかく、何度も繰り返して行わなければならないような場合には不利になります。このような処理のために用意されているのが、スレッドプールと呼ばれるものになります。

スレッドプールの概念図を下図に示します。スレッドプールはその名の通り、アプリケーション処理を動作させるためのスレッドがあらかじめプールされており、そこに処理が割り当てられて動作する、という形になります。

image

さきほどの例を、スレッドプールを使う方法に書き変えたコードを以下に示します。(※ このコードはよいコードではありません。理由は後述。)

   2: {
   3:     static void Main(string[] args)
   4:     {
   5:         int a = 0;
   6:         int b = 0;
   7:         ThreadPool.QueueUserWorkItem(new WaitCallback(AsyncTaskForPool), null);
   8:         Console.ReadLine();
   9:     }
  10:     
  11:     static void AsyncTaskForPool(object o)
  12:     {
  13:         int i = 0;
  14:         while (true)
  15:         {
  16:             i++;
  17:             Thread.Sleep(500);
  18:             Console.WriteLine("タスク実行中..." + i.ToString());
  19:         }
  20:     }
  21: }

先のコードと違って、スレッドを開始させるためのコードが書かれていないことに注意してください。スレッドプールを使う際には、動かしたい処理(メソッド)を、WaitCallback オブジェクトにラッピングして、ワークアイテムキューに放り込みます。このようにすると、スレッドプールの仕組みが自動的に機能し、ワークアイテムキューに放り込まれた処理を自動的に動かしてくれるようになっています。

[スレッドプールと ASP.NET アプリケーション]

さて、このスレッドプールは、以下のような特性を持つ処理をバックグラウンドで動作させる目的には適していません。

  • その処理が長時間を要する場合。

これは、以下のような理由によります。

  • スレッドプールは、.NET Framework の様々な場所で利用されている。

    具体的には、データベースアクセス、XML Web サービス呼び出しなどで利用されています。
  • スレッドプールは、プロセスの中にひとつしかなく、既定では 25 x CPU 数しかない。

    スレッドプール内のスレッドが枯渇すると、キューイングされているワークアイテムの処理が停止する
  • 長時間要するような処理をプールスレッド上で実行すると、その処理がプールスレッドを占有してしまうため、プールスレッドが枯渇し、キューイングされたワークアイテムが処理されなくなる恐れがある。(このため、数秒間で終わらないような長時間を要する処理は、プールスレッドではなく、マニュアルスレッドで動作させた方がよい。)

このため、スレッドプールは、以下のような特性を持つ処理をバックグラウンドで動作させる目的に適しています。

  • 細かい処理(短時間で終わる処理)を、大量に繰り返して実行しなければならない場合。

この典型例が、ASP.NET アプリケーションです。ASP.NET において各ページをマルチスレッド処理したいような場合、スレッドの起動/終了が大量に発生すると、オーバヘッドが非常に大きくなります。しかし、各ページをプールスレッドで処理すると、非常に効率的に処理することができます。

image

[ASP.NET アプリケーションにおけるスレッド構造]

ASP.NET アプリケーションの場合には、メインスレッドでは ASP.NET ランタイムの起動処理などが行われます。そして、入ってくるリクエストは、プールスレッドで処理されることになります。

では、これを具体的な例を使って説明してみましょう。たとえば、今、以下のような Web サイトがあったとします。

image

ここで、PageA, PageB では以下のようなロジックが書かれていたとします。(本来は PageA のようにローカル変数としてビジネスロジッククラスのインスタンスを生成する方がよいのですが、ここではサンプルとして PageB のようなコードも書いてみます。)

   1: public partial class PageA : System.Web.UI.Page
   2: {
   3:     protected void Page_Load(object sender, EventArgs e)
   4:     {
   5:         BizLogicA objBizA = new BizLogicA();
   6:         objBizA.MethodX();
   7:     }
   8: }

   1: public partial class PageB : System.Web.UI.Page
   2: {
   3:     BizLogicB objBizB = new BizLogicB();
   4:  
   5:     protected void Page_Load(object sender, EventArgs e)
   6:     {
   7:         objBizB.MethodY();
   8:     }
   9: }

今、この Web サイトに対して、ある瞬間に、二人のユーザが PageA.aspx を、別の一人のユーザが PageB.aspx を呼び出したとします。この場合には、以下のようになります。

  • 3 つのプールスレッドが動作し、それぞれ PageA, PageA, PageB を処理する。
  • 各スレッドごとにページクラスのインスタンスが作成され、それが動作する。

image

メモリ構造としては、以下のような形になります。(メインスレッドと、インストラクションコードは省略して示します。線を引いてみたけどかえって見づらくなってしまったかも....)

image

[以上を元に考えてみると。]

というわけで、マニュアルスレッドやプールスレッドが利用される場合のメモリ構造について解説をしてきたわけですが、だいたいここまでの知識を元にすれば、前回のエントリのクイズに関しては答えがわかると思います。ちょっとだけヒントを書いておくと、

  • MethodA, MethodB についてはほぼ自明。
  • MethodC はひっかけ問題。ある条件ではマルチスレッドアプリで使っても OK だが、ある条件だと NG。(よってスレッドセーフではない)

になります。

[まとめ]

本エントリでの解説のキーポイントをまとめると、次のようになります。

  • .NET アプリケーション内部で使われる主なスレッドには、以下の 3 種類がある。

    ① メインスレッド

    ② マニュアルスレッド

    ③ プールスレッド

  • 長時間処理をバックグラウンドで行いたいような場合には、マニュアルスレッドを使う。
  • 短時間処理をたくさん捌かなければならないような場合には、プールスレッドを使う。
  • ASP.NET ランタイムは、プールスレッドを使って、複数ユーザからのリクエストをマルチスレッド処理している。

というわけで一気に書き上げてみましたが、結構大変でした;。この説明でわかってもらえるとよいのですが、どうでしょう?^^

Comments (8)

  1. けろ-みお より:

    お世話になっております。1つ確認にさせてください。

    >スレッドプールは、プロセスの中にひとつしかなく、既定では 25 x CPU 数しかない。

    「25 x CPU 数」というのは、スレッド数のことを指していらっしゃるのでしょうか?

    もし、スレッド数を指しているのであれば、必ずしも「25 x CPU 数」とはならない気がします。

    Frameworkのバージョンによっても異なり、.NET Framework 2.0 SP1では、25から、250に引き上げられました。

    http://www.bluebytesoftware.com/blog/CommentView,guid,ca22a5a8-a3c9-4ee8-9b41-667dbd7d2108.aspx

    もし、意図があり、「25 x CPU 数」とおっしゃられているのであれば、申し訳ございません。

  2. nakama より:

    けろ-みおさん、フォローありがとうございます。

    っと、ホントですね。すみません、SP1 での修正は押さえていませんでした。

    情報、ありがとうございます。

    ※ ちなみに以下のコードで確認できます。

    int workerThreads, completionPortThreads;

    ThreadPool.GetAvailableThreads(out workerThreads, out completionPortThreads);

    Console.WriteLine(workerThreads);

    Console.WriteLine(completionPortThreads);

    スレッドプール数については、確か aspnet_wp.exe などでは独自調整が

    可能になっていたり、ワーカスレッドと I/O 完了ポートスレッドがあったりと、

    結構細かい話があるんですよね。(白状すると、未だ I/O 完了ポートスレッドが

    イマイチ理解できてないです....)

    話の趣旨は 25xCPU でも 250xCPU でも変わりませんが(長時間処理をプール

    スレッド上で行うべきではないという話は変わりがない)、スレッド枯渇

    問題はかなり緩和される、ということでしょうね。

  3. kazenami より:

    前回のオブジェ期とインスタンスに引き続き、

    スレッドについて詳細な解説どうもありがとうございます!

    メインスレッド、マニュアルスレッドがどのように開始されるのか理解できました(^_^)v

    これで、妙なところからスレッドが発生して、同じインスタンスを操作するといった心配はなくなりました。

    しかし、クイズの方はお正月からずっと悩んでいたのですが、

    やはり分からず、そろそろギブアップ(+_+;)気味です。。。。

    MethodCはstaticな変数を操作しておらず、

    一見スレッドセーフのようですが、

    今回の解説を考え合わせて見ると、上記の例のように

    Class Xのオブジェクトを、例えば(任意のクラス)Class Yで

    作成し、それをメインスレッド、マニュアルスレッドの両方から

    操作した場合はスレッドセーフじゃないのか!?

    などなど、悶々とした日々をすごしております。

    で、できたら種明かしをお願いしたいのですが。。。

  4. 通りすがり より:

    通りすがりで何なんですが、

    MethodCについて、私も種明かしをお願いしたいです。

    引数yの方にも注目する必要がありますか?

  5. nakama より:

    ^^ では、もう時間も経ちましたので、種明かしということで。MethodC に関しては、

    ・スレッドごとにインスタンスを生成し、生成したスレッド内でそのインスタンスの MethodC を実行する分には問題が発生しない。

    ・しかし、1 つのインスタンスを複数のスレッドで共有し、共有したインスタンスの MethodC を呼び出す場合にはスレッドセーフにはならない。

    となります。例えば、Web アプリケーションを例にとっていうと、A.aspx というページ上でこの Class X を使う場合、

    public partial class A : Page

    {

    private X objX = new X();

    protected void Page_Load(object sender, EventArgs e)

    {

    objX.MethodC();

    }

    }

    とする場合には問題が発生しませんが、

    public partial class A : Page

    {

    private static X objX = new X();

    protected void Page_Load(object sender, EventArgs e)

    {

    objX.MethodC();

    }

    }

    とした場合にはアウト、ということになります。

  6. というわけでまたしてもかなり日にちが空いてしまいました;。年度末ということもあって仕事が立て込んでいたのですが、ほぼ一段落したので久しぶりにエントリを。どうしてもまとまった話題を書こうとすると時間がかかっちゃいますね....

  7. さて、Windows フォームは、Windows OS が持つ様々なウィンドウ制御の仕組みに基づいて開発されている UI 技術です。このため、Windows フォームのマルチスレッド処理を理解するためには、まず

  8. さて、前回のエントリでは、Windows フォーム内部におけるスレッドの構成や、メッセージループの働きなどについて解説しました。中でも重要なこととして、以下のようなキーポイントがありました。 UI スレッド上で、長時間処理を動かしてはならない。

Skip to main content