C#でマルチスレッドプログラミング−3
Parallelクラスを使った並列ループ
.NET Framework4からはSystem.Threading.Tasks 名前空間が提供されていて、より簡単にかけるようになっているようだ。ありがたや、ありがたや。
Parallelクラスを使うとForループを簡単に並列化できる。
using System; using System.Threading.Tasks; class Program { //1スレッドでのループ回数 private const int NUM_LOOP = 10000000; //100以下の自然数の一様乱数をNUM_LOOP個取得してその和を表示 static void ParallelizedMethod(int id) { Random x = new Random(id); long result = 0; for (int i = 0; i < NUM_LOOP; i++) { result += x.Next(100); } Console.WriteLine("Thread ID:{0}, Value : {1}", id, result); } static void Main(string[] args) { //ParallelizedMethodをそれぞれ3回実行 Console.WriteLine("correct expression"); Parallel.For(0, 3, new Action<int>(ParallelizedMethod)); //省略形の以下でもOK Console.WriteLine("shoter expression"); Parallel.For(0, 3, ParallelizedMethod); //Foreach版 Console.WriteLine("In case of ForEach"); Parallel.ForEach(new int[3]{0,1,2}, ParallelizedMethod); } }
実行結果
correct expression Thread ID:0, Value : 494982776 Thread ID:1, Value : 494781791 Thread ID:2, Value : 495154121 shoter expression Thread ID:0, Value : 494982776 Thread ID:2, Value : 495154121 Thread ID:1, Value : 494781791 In case of ForEach Thread ID:2, Value : 495154121 Thread ID:1, Value : 494781791 Thread ID:0, Value : 494982776
排他制御
まずは排他制御なしで実行。スレッドを20個用意して、そのそれぞれでsum(初期値:100)から数を引いていくような処理を実行。
計算結果が負にならない時にだけ引き算するようにしているつもりが・・・
using System; using System.Threading.Tasks; class Program { //全スレッドから参照される合計値 private static int sum = 100; static void Main(string[] args) { //スレッド20個用意して、スレッドの番号を適当に引いていく Parallel.For(1, 20, i => { for (int j = 0; j < 100; j++) { //計算結果が負にならないときだけ実行(しているつもり) if ((sum - i) > 0) { Console.WriteLine("Current sum : " + sum); Console.WriteLine("Withdraw : -" + i); sum = sum - i; Console.WriteLine("New sum : " + sum); } } }); //計算終了時のsum Console.WriteLine("Result : {0}", sum); } }
実行結果
Current sum : 100 Current sum : 100 Withdraw : -13 New sum : 87 Current sum : 87 Withdraw : -13 New sum : 74 Current sum : 74 Withdraw : -13 New sum : 61 Current sum : 61 Withdraw : -13 New sum : 48 Current sum : 48 Withdraw : -13 New sum : 35 Current sum : 35 Withdraw : -13 New sum : 22 Current sum : 22 Withdraw : -13 New sum : 9 Current sum : 9 Current sum : 100 Withdraw : -9 New sum : 0 Withdraw : -5 New sum : -5 Current sum : 100 Current sum : 100 Withdraw : -17 New sum : -22 Withdraw : -1 New sum : -23 Withdraw : -2 New sum : -25 Result : -25
最終計算結果がマイナスになってしまう。これは変数を参照するときにきちんと排他制御していないために起こるので排他制御を導入する。
クリティカルセクション(排他制御をかけてる箇所)は同期オブジェクト(排他制御する際のキーとなるオブジェクト)を使って以下のように書く。
同期オブジェクト自体はなんでもいいっぽい。
Object thisLock = new Object();
lock (thisLock)
{
// Critical code section.
}
これを参考にコードを修正。
using System; using System.Threading.Tasks; class Program { //全スレッドから参照される合計値 private static int sum = 100; static void Main(string[] args) { //同期オブジェクトの生成 Object thisLock = new Object(); //スレッド20個用意して、スレッドの番号を適当に引いていく Parallel.For(1, 20, i => { for (int j = 0; j < 100; j++) { lock (thisLock) { //計算結果が負にならないときだけ実行 if ((sum - i) > 0) { Console.WriteLine("Current sum : " + sum); Console.WriteLine("Withdraw : -" + i); sum = sum - i; Console.WriteLine("New sum : " + sum); } } } }); //計算終了時のsum Console.WriteLine("Result : {0}", sum); } }
実行結果
Current sum : 100 Withdraw : -1 New sum : 99 Current sum : 99 Withdraw : -1 New sum : 98 Current sum : 98 Withdraw : -5 New sum : 93 Current sum : 93 Withdraw : -5 New sum : 88 Current sum : 88 Withdraw : -5 New sum : 83 Current sum : 83 Withdraw : -5 New sum : 78 Current sum : 78 Withdraw : -5 New sum : 73 Current sum : 73 Withdraw : -5 New sum : 68 Current sum : 68 Withdraw : -5 New sum : 63 Current sum : 63 Withdraw : -13 New sum : 50 Current sum : 50 Withdraw : -17 New sum : 33 Current sum : 33 Withdraw : -17 New sum : 16 Current sum : 16 Withdraw : -9 New sum : 7 Current sum : 7 Withdraw : -1 New sum : 6 Current sum : 6 Withdraw : -5 New sum : 1 Result : 1
ちゃんと答えが負にならない範囲で計算が終了する。