┏第21号━━━━━━━━━━━━━━━━━━━━━━━━━━┓ ┃         .NETプログラミング研究         ┃ ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ 〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜メニュー ■.NET Tips ・.NETのマルチスレッドプログラミング その3 - スレッドの同期 〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜メニュー ─────────────────────────────── ■.NET Tips ─────────────────────────────── ●.NETのマルチスレッドプログラミング その3 ★スレッドの同期 マルチスレッドプログラミングでの難所はスレッドの同期の問題でし ょう。「あるスレッドで実行される処理が終わらなければ別のスレッ ドの処理を実行できない」というような場合にスレッドの同期が必要 になります。 さらにその中でも同期が必要なケースとして重要でありながら、初心 者には分かり難いのが、複数のスレッドが同じオブジェクトにアクセ スする時に必要となる同期です。ここではその、複数のスレッドが共 有オブジェクトにアクセスする時の同期の方法について、基本を説明 します。 はじめに、複数のスレッドが同じオブジェクトにアクセスする時にな ぜ同期が必要になるのか説明します。まずは下のコードをご覧くださ い。 '[VB.NET]・・・・・・・・・・・・・・・・・・・・・・・・・・ 'Imports System.Threading 'が宣言されているものとする Class [MyClass] Private count As Integer = 0 Public Sub Increment() 'countが10より小さい時は1増やす If count < 10 Then count += 1 '(何らかの処理) '確実に10回以下しか実行されないか? End If End Sub End Class Class MainClass 'エントリポイント Public Shared Sub Main() Dim cls As New [MyClass] '複数のスレッドを作成し、開始する Dim i As Integer For i = 0 To 99 Dim t As New Thread( _ New ThreadStart(AddressOf cls.Increment)) t.Start() Next i Console.ReadLine() End Sub End Class '・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・ //[C#]・・・・・・・・・・・・・・・・・・・・・・・・・・・・ //using System.Threading; //が宣言されているものとする class MyClass { private int count = 0; public void Increment() { //countが10より小さい時は1増やす if (count < 10) { count++; //(何らかの処理) //確実に10回以下しか実行されないか? } } } class MainClass { //エントリポイント public static void Main() { MyClass cls = new MyClass(); //複数のスレッドを作成し、開始する for (int i = 0; i < 100; i++) { Thread t = new Thread(new ThreadStart(cls.Increment)); t.Start(); } Console.ReadLine(); } } //・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・ 上記のプログラムでは、MyClass.Incrementメソッド内の「(何らかの 処理)」の部分が多くても10回しか実行されないように、countフィー ルドでカウントし、制御しています。果たして、複数のスレッドから このIncrementメソッドを呼び出したときでも、「(何らかの処理)」 の部分が11回以上実行されることは絶対にないでしょうか? 実は、十分ありえます。例えばcountの値が9の時、あるスレッドが「 if (count < 10)」の条件にパスし、「count++」(VB.NETでは、「 count += 1」)を処理するまでの間に、別のスレッドが「if (count < 10)」を処理した場合、countの値はまだ9のままのためそのスレッド でも条件にパスしてしまい、結果として両方のスレッドで「(何らか の処理)」が実行されてしまいます。このようにタイミングの違いに より結果が変わってしまうという問題は、複数のスレッドから共有リ ソースへのアクセスにより起こりうる典型的な現象で、「競合状態( Race Condition)」と呼ばれています。 このような競合状態を回避するために、スレッドの同期が必要となり ます。そのためには、MyClass.Incrementメソッドを次のように書き換 えます。 '[VB.NET]・・・・・・・・・・・・・・・・・・・・・・・・・・ 'Imports System.Threading 'が宣言されているものとする Class [MyClass] Private count As Integer = 0 Public Sub Increment() 'ロックを取得 Monitor.Enter(Me) 'countが10より小さい時は1増やす If count < 10 Then count += 1 '(何らかの処理) '確実に10回以下しか実行されない End If 'ロックを開放 Monitor.Exit(Me) End Sub End Class '・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・ //[C#]・・・・・・・・・・・・・・・・・・・・・・・・・・・・ //using System.Threading; //が宣言されているものとする class MyClass { private int count = 0; public void Increment() { //ロックを取得 Monitor.Enter(this); //countが10より小さい時は1増やす if (count < 10) { count++; //(何らかの処理) //確実に10回以下しか実行されない } //ロックを開放 Monitor.Exit(this); } } //・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・ MyClass.Incrementメソッドの先頭と末尾に Monitor.Enter(this); と Monitor.Exit(this); を挿入しただけですが、これでこの間のコードブロックには1つのスレ ッドしか入れなくなります。(このように複数のスレッドが同時に実 行しないことが保証された領域のことを「クリティカルセクション」 と呼びます。) Monitor.Enterメソッドは指定されたオブジェクトに対する排他ロック を取得し、Monitor.Exitメソッドは排他ロックを解放します。あるス レッドがMonitor.Enterにより、あるオブジェクト(ここではthis)を ロックすると、他のスレッドはMonitor.Enterで同じオブジェクトのロ ックを取得することができず、ロックを取得しているスレッドが Monitor.Exitを呼び出してロックを解放するまでブロックされます。 つまり、Monitor.EnterとMonitor.Exitで囲まれた部分は一つのスレッ ドでしか実行できなくなり、2つ目以降のスレッドがアクセスしようと すると待機させられます。 Monitorはオブジェクト(参照型)をロックするために使われるため、 Monitor.Enterメソッドに値型の変数を渡してはいけないということに 注意してください。(詳しくはヘルプ「Monitor」をご覧ください。) ・Monitor http://www.microsoft.com/japan/msdn/library/ja/cpguide/html/cpconmonitor.asp Monitor.Enterメソッドは複数回呼び出すこともできますが、このとき は同じ数だけMonitor.Exitで解放します。 (補足:クリティカルセクションはできるだけ短くすべきです。上記 のコードでも場合によってはメソッド全体をクリティカルセクション にする必要はないでしょう。) 上記のMonitor.EnterとMonitor.Exitで囲むだけの方法では多少問題が あります。というのは、例えばMonitor.Enterが呼び出されてから Monitor.Exitが呼び出されるまでの間にエラーが発生した時、ロック が解除されないということになってしまいます。これを回避するため には、try...finally...のfinally句内にMonitor.Exitを書いておけば よいのですが、これと全く同じことがlock(VB.NETではSyncLock)ステー トメントで出来てしまいます。よって通常はこちらを使うべきです。 先のコードをlockステートメントを使って書き直すと次のようになり ます。 '[VB.NET]・・・・・・・・・・・・・・・・・・・・・・・・・・ Class [MyClass] Private count As Integer = 0 Public Sub Increment() SyncLock Me 'countが10より小さい時は1増やす If count < 10 Then count += 1 '(何らかの処理) End If End SyncLock End Sub End Class '・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・ //[C#]・・・・・・・・・・・・・・・・・・・・・・・・・・・・ //using System.Threading; //が宣言されているものとする class MyClass { private int count = 0; public void Increment() { lock(this) { //countが10より小さい時は1増やす if (count < 10) { count++; //(何らかの処理) } } } } //・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・ 静的メソッド内でクリティカルセクションを指定する時は、ロックす るオブジェクトとして、Typeオブジェクトを使用するのが一般的です。 次にその例を示します。 '[VB.NET]・・・・・・・・・・・・・・・・・・・・・・・・・・ 'Imports System.Threading 'が宣言されているものとする Class MainClass Private Shared count As Integer = 0 'エントリポイント Public Shared Sub Main() '複数のスレッドを作成し、開始する Dim i As Integer For i = 0 To 99 Dim t As New Thread(New ThreadStart(AddressOf MyThread)) t.Start() Next i Console.ReadLine() End Sub Public Shared Sub MyThread() SyncLock GetType(MainClass) 'countが10より小さい時は1増やす If count < 10 Then count += 1 '(何らかの処理) Console.WriteLine(count) End If End SyncLock End Sub End Class '・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・ //[C#]・・・・・・・・・・・・・・・・・・・・・・・・・・・・ //using System.Threading; //が宣言されているものとする class MainClass { private static int count = 0; //エントリポイント public static void Main() { //複数のスレッドを作成し、開始する for (int i = 0; i < 100; i++) { Thread t = new Thread(new ThreadStart(MyThread)); t.Start(); } Console.ReadLine(); } public static void MyThread() { lock(typeof(MainClass)) { //countが10より小さい時は1増やす if (count < 10) { count++; //(何らかの処理) Console.WriteLine(count); } } } } //・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・ メソッド全体をクリティカルセクションとするには、 MethodImplAttributeとMethodImplOptions.Synchronizedを使用するこ ともできます。例えば先のMyClass.Incrementメソッド全体をクリティ カルセクションにするには次のようにします。 '[VB.NET]・・・・・・・・・・・・・・・・・・・・・・・・・・ 'Imports System.Threading 'Imports System.Runtime.CompilerServices 'が宣言されているものとする Class [MyClass] Private count As Integer = 0 _ Public Sub Increment() 'countが10より小さい時は1増やす If count < 10 Then count += 1 '(何らかの処理) Console.WriteLine(count) End If End Sub End Class '・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・ //[C#]・・・・・・・・・・・・・・・・・・・・・・・・・・・・ //using System.Threading; //using System.Runtime.CompilerServices; //が宣言されているものとする class MyClass { private int count = 0; [MethodImpl(MethodImplOptions.Synchronized)] public void Increment() { //countが10より小さい時は1増やす if (count < 10) { count++; //(何らかの処理) Console.WriteLine(count); } } } //・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・ 最後に、マルチスレッドプログラミングで「競合状態」とともに注意 すべき問題である「デッドロック」について説明します。 デッドロックとは、複数のスレッドがお互いにブロックし合い、身動 きができなくなる状況のことをいいます。次にその典型的な例を示し ましょう。 '[VB.NET]・・・・・・・・・・・・・・・・・・・・・・・・・・ 'Imports System.Threading 'が宣言されているものとする Class MainClass Private Shared o1 As New Object Private Shared o2 As New Object 'エントリポイント Public Shared Sub Main() '別スレッドを作成し、開始する Dim t As New Thread(New ThreadStart(AddressOf MyMethod)) t.Name = "1" t.Start() 'o1をロックする SyncLock o1 Console.WriteLine("メインスレッドでo1をロック") 'しばらく待機 Thread.Sleep(2000) 'o2をロックする 'デッドロックとなる SyncLock o2 Console.WriteLine("メインスレッドでo2をロック") End SyncLock End SyncLock Console.WriteLine("メインスレッド終了") End Sub Private Shared Sub MyMethod() 'しばらく待機 Thread.Sleep(1000) 'o2をロックする SyncLock o2 Console.WriteLine("スレッド1でo2をロック") 'o1をロックする 'デッドロックとなる SyncLock o1 Console.WriteLine("スレッド1でo1をロック") End SyncLock End SyncLock Console.WriteLine("スレッド1終了") End Sub End Class '・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・ //[C#]・・・・・・・・・・・・・・・・・・・・・・・・・・・・ private static object o1 = new object(); private static object o2 = new object(); //エントリポイント public static void Main() { //別スレッドを作成し、開始する Thread t = new Thread(new ThreadStart(MyMethod)); t.Name = "1"; t.Start(); //o1をロックする lock(o1) { Console.WriteLine("メインスレッドでo1をロック"); //しばらく待機 Thread.Sleep(2000); //o2をロックする //デッドロックとなる lock(o2) { Console.WriteLine("メインスレッドでo2をロック"); } } Console.WriteLine("メインスレッド終了"); } private static void MyMethod() { //しばらく待機 Thread.Sleep(1000); //o2をロックする lock(o2) { Console.WriteLine("スレッド1でo2をロック"); //o1をロックする //デッドロックとなる lock(o1) { Console.WriteLine("スレッド1でo1をロック"); } } Console.WriteLine("スレッド1終了"); } //・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・ 上のコードを実行させると、まずメインスレッドでオブジェクト"o1" をロックし、スレッド"1"でオブジェクト"o2"をロックします。その後、 スレッド"1"は"o1"のロックを要求しますが、すでにメインスレッドで ロックされているため、メインスレッドでロックが解放されるまでブ ロックされます。片やメインスレッドでも"o2"のロックを要求します が、"o2"はスレッド"1"ですでにロックされているため、やはり解放さ れるまでブロックされます。結局双方のスレッドがお互いをブロック し、全く身動きができなくなってしまいます。 デッドロックを避けるためには、むやみに同期機構を使わない、入念 に計画する、Monitor.TryEnterでタイムアウトするようにする、など の対策が考えられます。 =============================== ■このマガジンの購読、購読中止、バックナンバー、説明に関しては  次のページをご覧ください。  http://www.mag2.com/m/0000104516.htm ■発行人・編集人:どぼん!  (Microsoft MVP for Visual Basic, Oct 2003-Oct 2004)  http://dobon.net  dobon@bigfoot.com ■ご質問等はメールではなく、掲示板へお願いいたします。  http://dobon.net/vb/bbs.html ■上記メールアドレスへのメールは確実に読まれる保障はありません  (スパム、ウィルス対策です)。メールは下記URLのフォームメール  から送信してください。  http://dobon.net/mail.html Copyright (c) 2003 DOBON! All rights reserved. ===============================