xin9le.net

Microsoft の製品/技術が大好きな Microsoft MVP な管理人の技術ブログです。

TPL入門 (2) - 単純なループ

今回はコレクション要素に対して同様の処理を並列に行う方法について考えます。そのシナリオをサポートするために、TPLではSystem.Threading.Tasks名前空間に並列実行可能なFor/ForEach文が提供されています。

Parallel.ForEach

最も簡単なParallel.ForEach文の例を示します。

using System;
using System.Diagnostics;
using System.Threading;
using System.Threading.Tasks;
 
namespace Sample01_ParallelForEach
{
    class Program
    {
        static void Main()
        {
            //----- データ生成
            var collection = new int[]{ 10, 20, 30, 40, 50 };
 
            //----- 通常ループ
            var stopwatch = Stopwatch.StartNew();
            foreach (var item in collection)
            {
                Console.WriteLine(item);
                Thread.Sleep(1000);
            }
            stopwatch.Stop();
            Console.WriteLine(stopwatch.ElapsedMilliseconds.ToString("Normal : 0[ms]"));
 
            //----- 並列ループ
            stopwatch.Restart();
            Parallel.ForEach(collection, item =>
            {
                Console.WriteLine(item);
                Thread.Sleep(1000);
            });
            stopwatch.Stop();
            Console.WriteLine(stopwatch.ElapsedMilliseconds.ToString("Parallel : 0[ms]"));
        }
    }
}

//----- 結果 : 環境によっては多少異なる場合があります。
/*
10
20
30
40
50
Normal : 5002[ms]
10
20
40
30
50
Parallel : 1010[ms]
*/

見て分かるように、各要素に対する処理をデリゲート/ラムダ式で書くところがポイントです。また、コードの雰囲気は通常のforeach文と比べて大きな違いはなく、さほど違和感がありません。しかし結果は大違いです。

通常のforeach文では単一のスレッドで処理されるため、「表示して1秒待って」を繰り返した結果、5秒かかっています。しかしParallel.ForEachでは複数のスレッドが立てられ、その各スレッドで「表示して1秒待って」が行われるため、結果として1秒しかかかっていません。つまり、僕の環境では5本のスレッドが立てられたということになります。実行される環境によっては、2秒かかったり3秒かかったりするかもしれません。

また、コレクション要素を取得し処理される順序は一定ではないため、気を付ける必要があります。基本的に、並列処理は順序に依存せずに処理できる場合に限られます。要素の並び順での処理が必要な場合は通常の書き方をしてください。

Parallel.For

Parallel.Forの書き方も以下に紹介します。

using System;
using System.Diagnostics;
using System.Threading;
using System.Threading.Tasks;
 
namespace Sample02_ParallelFor
{
    class Program
    {
        static void Main()
        {
            //----- データ生成
            var collection = new int[]{ 10, 20, 30, 40, 50 };
 
            //----- 通常ループ
            var stopwatch = Stopwatch.StartNew();
            for (int index = 0; index < collection.Length; index++)
            {
                Console.WriteLine(collection[index]);
                Thread.Sleep(1000);
            }
            stopwatch.Stop();
            Console.WriteLine(stopwatch.ElapsedMilliseconds.ToString("Normal : 0[ms]"));
 
            //----- 並列ループ
            stopwatch.Restart();
            Parallel.For(0, collection.Length, index =>
            {
                Console.WriteLine(collection[index]);
                Thread.Sleep(1000);
            });
            stopwatch.Stop();
            Console.WriteLine(stopwatch.ElapsedMilliseconds.ToString("Parallel : 0[ms]"));
        }
    }
}

//----- 結果 : 環境によっては多少異なる場合があります。
/*
10
20
30
40
50
Normal : 5002[ms]
10
30
50
20
40
Parallel : 1013[ms]
*/

indexのインクリメントが自動で行われることを除けば、基本的には通常のfor文と遜色ない容易さで記述できます。ただし、インデックスアクセスにおける記述に特化しているため、通常のfor文ほど汎用的でないという点で注意が必要です。インデックスアクセスによるfor文の記述を並列化する場合に利用できる、と覚えておくと良いと思います。結果はParallel.ForEachでの結果と似た感じになります。

導入の容易さ

どのようなプログラムでも実行時間を短縮できるわけではありませんが、何と言っても非常に手軽に導入できるところが魅力です。以前に作成したアプリケーションを複数コアに対応した形に書き直すことを考えると、Parallelクラスを利用するのは非常に有用と言えます。

次回予告

今回は、コレクション要素に対するループについて見てきました。次回はこれらの挙動をもう少し視覚的に観察していきたいと思います。