【ワレコのC#】非同期Taskをasync/await並列実行【マルチスレッド】

この記事は約14分で読めます。
スポンサーリンク

ワテの場合、かなり前にMPICHやOpenMPとかのライブラリを使ってC++で並列実行するプログラムを作成した経験がある。ある種のシミュレーションプログラムだ。

WindowsのVisual StudioとLinuxのg++との両方でコンパイル出来るようにソースコードを書いた記憶がある。

開発には普通のWindowsパソコンを使っていたが、実際の実行環境は64CPUくらいのLinuxマシンなどを使った。

実行したプログラムが複数のCPUを使って猛烈に並列実行され、互いのプロセス間で途中計算結果をメッセージパッシングして同期を取る。

そんな感じでシステムのCPUが100%フル稼働状態で並列で高速に計算が行われる様子を見ていると、ある種の快感がある(ワテの場合)。

やっぱりプログラミングの醍醐味は、並列処理だよなぁ~。

最近のパソコンのCPUは20年くらい前の超高速スーパーコンピューターの数万倍くらいの計算能力を持っていると思う(ワテの単なる推測)。

そんな高性能パソコンをネットサーフィン程度にしか使わないのは勿体ない。

と言う事で、この記事ではマイクロソフト社のVisual StudioのC#言語を使って、簡単な並列マルチスレッド計算処理をする実験をしてみたので、それを備忘録としてまとめてみた。

結論としては、簡単にマルチスレッドタスクを並列実行する事に成功した。

では本題に入ろう。

ちなみにワテの開発環境は以下の通り。

  • Windows10 Pro 64
  • CPU Core i7-4770K Haswell (3.5GHz)
  • MEM CFD W3U1600HQ-8GC11 [DDR3 PC3-12800 8GB 2枚組] 計32GB
  • SSD Crucial Micron製 750GBなど
  • Visual Studio 2017 Communityなど

マシンの詳細は

【ワテ本番機】Win8をWin10に無料アップグレード完了

に記載している。

スポンサーリンク
ワテ推薦のプログラミングスクールで学ぶ
スポンサーリンク
スポンサーリンク

C#コンソールアプリケーションでasync/awaitの非同期処理を行う

さて、今回はC#のコンソールアプリにおいて、10個のスレッドを並列実行して、全てが完了した時点で次の処理に進むと言うサンプルを作ってみた。

さらに、その10個のスレッド実行中にどれかのスレッドで例外が発生した場合には、Main関数にその情報を伝えるようにした。

こう言う状況は、並列計算をする場合には良く起こり得るので、このサンプルを応用すればいろんな並列処理に使えると思う。

なお、今回作成したプログラムでは、10個のスレッドは互いに独立していて、何らかの変数や情報を共有したり交換したりする処理は入れていない。

なので、最も単純な並列計算になる。

本当は、互いのスレッド間でデータ交換するようなサンプルのほうが応用が利くと思うが、それは次回の記事でやってみたい。

前置きが長くなったが、今回作成したサンプルプログラムは以下の通り。

10個の非同期スレッドタスクを実行して全部の完了を待つコンソールアプリ

using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
 
namespace _2017_10_31_CallMultipleTasksAndWaitUntilAllDone
{
    class Program
    {
        static void Main(string[] args)
        {

#if(false)
            var result = MyAsyncAwaitTest1();           // これだと終了結果を待たずに終わってしまう。
#else
            var result = MyAsyncAwaitTest1().Result;    // ok
            if (result == 0)
            {
                Console.WriteLine("result={0} 正常終了", result);
            }
            else
            {
                Console.WriteLine("result={0} 何かエラー発生", result);
            }
#endif
        }
 
 
        static private async Task<int> MyAsyncAwaitTest1()
        {
            try
            {
                var tasks = new List<Task>();
                for (int i = 0; i < 10; i++)
                {
                    tasks.Add(Method1(i));
                }
                await Task.WhenAll(tasks);
                Console.WriteLine("Tasks All done!");
                return 0;
            }
            catch (Exception ex)
            {
                return -1;
            }
        }
 
        static private async Task<int> Method1(int i)
        {
            /**
             * 並列実行する何らかの処理
             * この例では、乱数で2~7の整数を発生させて、その秒数だけDelayするだけの処理。
             **/
 
            var random = StaticRandom.Instance.Next(2, 7);
            if (random == 2)                            // 処理中に何らかの例外が発生する状況を
            {                                           // シミュレートする為に、例えば==2の時に
                throw new ApplicationException();       // 例外をスローしてみた。
            }
 
            var milliSec = random * 1000;
        //x System.Threading.Thread.Sleep(milliSec);    // Sleepすると並列実行にならない。
            await Task.Delay(milliSec);                 // Delayなら並列実行出来る。
            Console.WriteLine("Task " + i + " done.");
            return i;
        }
 
        static public class StaticRandom
        {
            /**
             * 整数の乱数を生成する関数
             * 引用元はスタックオーバーフローのサイト
             * https://stackoverflow.com/questions/767999/random-number-generator-only-generating-one-random-number?noredirect=1&lq=1
             * Random number generator only generating one random number
             **/
 
            private static int seed;
 
            private static ThreadLocal<Random> threadLocal = new ThreadLocal<Random>
                (() => new Random(Interlocked.Increment(ref seed)));
 
            static StaticRandom()
            {
                seed = Environment.TickCount;
            }
 
            public static Random Instance { get { return threadLocal.Value; } }
        }
    }

自分のパソコンで試してみたい人は以下の手順でやればよい。

Visual Studio 2017 CommunityのC#のコンソールプロジェクトを新規作成する。

.NET Framework 4.7を使ったが、4.6.1や4.6.2などでも良いと思う。あまり古いバージョンを使うと、最新の機能が使えないなどがあるので特に理由が無ければ新しい.NET Frameworkを使うと良いだろう。

プログラムの説明

static void Main(string[] args)

まずはメイン関数。

C#のコンソールアプリには必ず必要だ。

static関数なので、以下の関数群も全部staticで作成した。

まあ、static無しでやりたい人は、適当なclassを作成してその中に関数群を入れておけば良いだろう。そのように定義したクラスをnewでインスタンス化(実体を作成)して使えば良い。

static private async Task<int> MyAsyncAwaitTest1()

この関数の中で、10個のタスクを非同期(async)で投げて、awaitでタスクの完了を待つ。

この関数の中で、下の様にTask.WhenAll() をawaitする事によって、全てのタスクが完了するまでここで待つ事が出来る。

await Task.WhenAll(tasks);

とっても便利な機能だ。

 

それで、並列実行する関数がこれだ。

static private async Task<int> Method1(int i)

引数は整数型にしてみた。その引数に与える値は、自分が何番目のタスクなのかを表すiにしてみた。

この関数の中では、2から7までの整数を乱数で発生させて、もし2なら例外をスローしている。

 var random = StaticRandom.Instance.Next(2, 7);
 if (random == 2)                            // 処理中に何らかの例外が発生する状況を
 {                                           // シミュレートする為に、例えば==2の時に
     throw new ApplicationException();       // 例外をスローしてみた。
 }

まあ、これは実際に応用した場合に計算中に何かエラーとか例外が発生した場合をシミュレートする目的で入れてみた。

ここで発生した例外は、その上位の関数MyAsyncAwaitTest1()の中でTry~Catchを入れているのでそこでキャッチ出来る。

 

さて、このMethod1()関数の後半では、以下のように発生した乱数の数字の秒数だけDelayすると言う単純な処理を入れている。

    var milliSec = random * 1000;
//x System.Threading.Thread.Sleep(milliSec);    // Sleepすると並列実行にならない。
    await Task.Delay(milliSec);                 // Delayなら並列実行出来る。
    Console.WriteLine("Task " + i + " done.");
    return i;

要するに10個並列で実行したそれぞれのタスクが、ここで3秒~7秒停止する訳だ。

これは、実際の応用では、何らかの計算処理が各スレッドで全く同じ時間で終わるとは限らず、現実的にはスレッドごとに計算に要する時間がばらつくからそれをシミュレートする目的で入れている。

ちなみに、この部分のDelay()をSleep()関数で置き換えたら、並列実行が出来なくなった。

Sleep()は非同期実行には対応していないのかな。たぶん。

乱数発生関数はStackOveflowから借用した

static public class StaticRandom

乱数の生成は簡単なようで難しい。

良くやる失敗としては、使うたびに同じ乱数系列が得られてしまうなどがある。

得られる数字自体は乱数的に並んでいるのだが、プログラムを何度実行しても前回と全く同じ乱数の数字の並びなのだ。

そう言う場合には乱数の種(Seed)を変化させるなどで異なる乱数系列を発生させる事が出来る。

ワテも昔、システムの時間を乱数種にするなどの小細工的な手法で乱数を発生させたりした記憶もある。でも最近の言語なら、seed用の関数も用意されていると思うのでそう言うのを使えば楽に乱数を生成出来る。

ところがC#にはseedを変化させる関数が無いのかな?

C# random seed site:stackoverflow.com

で検索すると多数の記事がヒットしたのだが、その中で、以下の記事にあったサンプルプログラムを今回引用して利用させて頂いた。

https://stackoverflow.com/questions/767999/random-number-generator-only-generating-one-random-number?noredirect=1&lq=1

なお、処理の詳細は各自調査して下さい。

ワテは良く分からないまま使っている。

実行結果

プログラムを実行するとコンソール画面が開いて、各スレッドからコンソールに自分の番号が表示される。

Task 4 done.
Task 2 done.
Task 5 done.
Task 0 done.
Task 8 done.
Task 6 done.
Task 7 done.
result=-1 何かエラー発生
続行するには何かキーを押してください . . .

でも乱数で生成した 2, 3, 4, 5, 6, 7 までの数字の2が出たら例外をスローするので殆どの場合、上の実行例のように途中でエラー発生してMainに戻る。

確率的に言うと、例外無しで10個のタスクが全部完了する確率は、

(5/6)10  =  9,765,625/60,4661,76 =  0.1615   かな?

(確率の計算なんて久しぶりなので間違っているかも知れないが。)

 

なので、10回くらい実行したら1回くらいは全部パスして下の画面が出るだろう。

Task 0 done.
Task 5 done.
Task 9 done.
Task 4 done.
Task 7 done.
Task 3 done.
Task 6 done.
Task 2 done.
Task 1 done.
Task 8 done.
Tasks All done!
result=0 正常終了
続行するには何かキーを押してください . . .

 

まとめ

この記事では、C#のコンソールアプリにおいて、asyncとawaitを使って複数のタスクを非同期で並列実行し、全部のタスクが終了するのを待って次の処理に進むと言うサンプルプログラムを説明した。

また、どれかのスレッドで何らかの例外が発生した場合には、その情報をMain関数に伝達してエラー発生が分るようにした。

サンプルプログラムに置いては、各スレッドでの計算時間のバラツキをシミュレートする目的で乱数発生関数を利用した。

それはStackOverflow.comを参照した。

なお、本文中でも説明したように、今回のサンプルプログラムでは、非同期で実行した複数のタスクは全く独立に実行される。

互いにデータ交換するなどの処理は一切入れていないので、最も単純なケースの並列処理だ。

でも実際の状況では、複数のスレッドが足並みを揃えて互いに相手の計算が終わるのを待って、データを同期させてから次の計算サイクルを実行するなどのパターンの方が多いだろう。

そう言う点では、今回のサンプルは応用範囲が狭い。

次回の記事では、その辺りを改良した汎用性のある並列計算のサンプルプログラムを紹介してみたい。

並列計算の本を読む

最近では並列計算の日本語の良い教科書も沢山あるようだ。

 

C#の並列計算の本を見付けた。

ワテの場合、まだ読んでいないが。

 

CPUではなくてGPUを使って並列計算させる技術は、ワテも大変興味あるのだが、その辺りはあまり詳しくない。

 

こう言うのを買って、並列計算プログラムを書いてバリバリ計算させてみたいなあ。

高いわ。

スポンサーリンク
ワテ推薦のプログラミングスクールで学ぶ
コメント募集

この記事に関して何か質問とか補足など有りましたら、このページ下部にあるコメント欄からお知らせ下さい。

C#
スポンサーリンク
warekoをフォローする
スポンサーリンク
われこ われこ

コメント