チュートリアル : Visual C# による簡単なマルチスレッド コンポーネントの作成
BackgroundWorker コンポーネントは、System.Threading 名前空間に代わると共に追加の機能を提供します。ただし、System.Threading 名前空間は、下位互換性を保つ目的および将来使用する目的で保持されます。詳細については、「BackgroundWorker コンポーネントの概要」を参照してください。
複数のタスクを同時に実行できるアプリケーションを作成できます。この機能は、"マルチスレッド" または "フリー スレッド" と呼ばれ、プロセッサ集中型でユーザー入力の必要なコンポーネントをデザインするための強力な手段です。マルチスレッドを利用するコンポーネントの例としては、給与支払い情報を計算するコンポーネントがあります。プロセッサ集中型の給与計算を 1 つのスレッドで実行している間に、ユーザーがデータベースに入力したデータを別のスレッドで処理できます。プロセスを個別のスレッドで実行することによって、ユーザーはコンピューターの計算処理が完了するのを待たずに追加のデータを入力できます。このチュートリアルでは、複数の複雑な計算を同時に実行する簡単なマルチスレッド コンポーネントを作成します。
プロジェクトの作成
作成するアプリケーションは、1 つのフォームと 1 つのコンポーネントで構成されます。ユーザーが値を入力してコンポーネントに通知すると、計算が開始されます。フォームはコンポーネントから値を受け取り、その値をラベル コントロールに表示します。このコンポーネントはプロセッサ集中型の計算を実行し、完了時にフォームに通知します。ユーザー インターフェイスから受け取った値を保持するために、コンポーネントにパブリック変数を作成します。また、これらの変数の値に基づいて計算を実行するためのメソッドをコンポーネントに実装します。
[!メモ]
一般に、値を計算する方法としては関数が適していますが、スレッド間で引数を渡したり値を返したりできません。スレッドに値を渡したり、スレッドから値を受け取ったりするための簡単な方法は多数あります。この例では、パブリック変数を更新して、ユーザー インターフェイスに値を返します。また、スレッドが実行を完了したときに、イベントを使ってメイン プログラムに通知します。
実際に画面に表示されるダイアログ ボックスとメニュー コマンドは、アクティブな設定またはエディションによっては、ヘルプの説明と異なる場合があります。設定を変更するには、[ツール] メニューの [設定のインポートとエクスポート] をクリックします。詳細については、「Visual Studio の設定」を参照してください。
フォームを作成するには
新しい Windows アプリケーション プロジェクトを作成します。
アプリケーションに Calculations という名前を付けて、Form1.cs の名前を frmCalculations.cs に変更します。Visual Studio によって、Form1 コード要素の名前を変更するように要求するメッセージが表示されたら、[はい] をクリックします。
このフォームは作成するアプリケーションのプライマリ ユーザー インターフェイスとして機能します。
5 つの Label コントロール、4 つの Button コントロール、および 1 つの TextBox コントロールをフォームに追加します。
各コントロールのプロパティを次のように設定します。
Control
名前
テキスト
label1
lblFactorial1
(空白)
label2
lblFactorial2
(空白)
label3
lblAddTwo
(空白)
label4
lblRunLoops
(空白)
label5
lblTotalCalculations
(空白)
button1
btnFactorial1
Factorial
button2
btnFactorial2
Factorial - 1
button3
btnAddTwo
Add Two
button4
btnRunLoops
Run a Loop
textBox1
txtValue
(空白)
Calculator コンポーネントを作成するには
[プロジェクト] メニューの [コンポーネントの追加] をクリックします。
コンポーネントに Calculator という名前を付けます。
Calculator コンポーネントにパブリック変数を追加するには
コード エディターで Calculator を開きます。
ステートメントを追加して、frmCalculations から各スレッドに値を渡すために使うパブリック変数を作成します。
変数 varTotalCalculations はコンポーネントによって実行された現在までの計算処理の合計件数を保持し、その他の変数はフォームから値を受け取ります。
public int varAddTwo; public int varFact1; public int varFact2; public int varLoopValue; public double varTotalCalculations = 0;
Calculator コンポーネントにメソッドとイベントを追加するには
コンポーネントがフォームに値を渡すために使用するイベントのデリゲートを宣言します。
[!メモ]
宣言するイベントは 4 つですが、2 つのイベントは同じシグネチャを持つため、作成する必要があるデリゲートは 3 つだけです。
前の手順で入力した変数宣言の直後に、次のコードを入力します。
// This delegate will be invoked with two of your events. public delegate void FactorialCompleteHandler(double Factorial, double TotalCalculations); public delegate void AddTwoCompleteHandler(int Result, double TotalCalculations); public delegate void LoopCompleteHandler(double TotalCalculations, int Counter);
コンポーネントがアプリケーションと通信するために使用するイベントを宣言します。これを行うには、前の手順で入力したコードの直後に次のコードを追加します。
public event FactorialCompleteHandler FactorialComplete; public event FactorialCompleteHandler FactorialMinusOneComplete; public event AddTwoCompleteHandler AddTwoComplete; public event LoopCompleteHandler LoopComplete;
前の手順で入力したコードの直後に、次のコードを入力します。
// This method will calculate the value of a number minus 1 factorial // (varFact2-1!). public void FactorialMinusOne() { double varTotalAsOfNow = 0; double varResult = 1; // Performs a factorial calculation on varFact2 - 1. for (int varX = 1; varX <= varFact2 - 1; varX++) { varResult *= varX; // Increments varTotalCalculations and keeps track of the current // total as of this instant. varTotalCalculations += 1; varTotalAsOfNow = varTotalCalculations; } // Signals that the method has completed, and communicates the // result and a value of total calculations performed up to this // point. FactorialMinusOneComplete(varResult, varTotalAsOfNow); } // This method will calculate the value of a number factorial. // (varFact1!) public void Factorial() { double varResult = 1; double varTotalAsOfNow = 0; for (int varX = 1; varX <= varFact1; varX++) { varResult *= varX; varTotalCalculations += 1; varTotalAsOfNow = varTotalCalculations; } FactorialComplete(varResult, varTotalAsOfNow); } // This method will add two to a number (varAddTwo+2). public void AddTwo() { double varTotalAsOfNow = 0; int varResult = varAddTwo + 2; varTotalCalculations += 1; varTotalAsOfNow = varTotalCalculations; AddTwoComplete(varResult, varTotalAsOfNow); } // This method will run a loop with a nested loop varLoopValue times. public void RunALoop() { int varX; double varTotalAsOfNow = 0; for (varX = 1; varX <= varLoopValue; varX++) { // This nested loop is added solely for the purpose of slowing down // the program and creating a processor-intensive application. for (int varY = 1; varY <= 500; varY++) { varTotalCalculations += 1; varTotalAsOfNow = varTotalCalculations; } } LoopComplete(varTotalAsOfNow, varLoopValue); }
コンポーネントへのユーザー入力の転送
次の手順では、ユーザーからの入力を受け取ったり、Calculator コンポーネントと値をやり取りしたりするためのコードを frmCalculations に追加します。
frmCalculations にフロントエンド機能を実装するには
コード エディターで frmCalculations を開きます。
public partial class frmCalculations ステートメントを探します。{ の直後に、次のコードを追加します。
Calculator Calculator1;
コンストラクターを探します。} の直前に、次の行を追加します。
// Creates a new instance of Calculator. Calculator1 = new Calculator();
デザイナーで各ボタンをクリックして、各コントロールの Click イベント ハンドラーのコード アウトラインを生成します。ここに各ハンドラーを作成するコードを追加します。
完了すると、Click イベント ハンドラーは次のようになります。
// Passes the value typed in the txtValue to Calculator.varFact1. private void btnFactorial1_Click(object sender, System.EventArgs e) { Calculator1.varFact1 = int.Parse(txtValue.Text); // Disables the btnFactorial1 until this calculation is complete. btnFactorial1.Enabled = false; Calculator1.Factorial(); } private void btnFactorial2_Click(object sender, System.EventArgs e) { Calculator1.varFact2 = int.Parse(txtValue.Text); btnFactorial2.Enabled = false; Calculator1.FactorialMinusOne(); } private void btnAddTwo_Click(object sender, System.EventArgs e) { Calculator1.varAddTwo = int.Parse(txtValue.Text); btnAddTwo.Enabled = false; Calculator1.AddTwo(); } private void btnRunLoops_Click(object sender, System.EventArgs e) { Calculator1.varLoopValue = int.Parse(txtValue.Text); btnRunLoops.Enabled = false; // Lets the user know that a loop is running lblRunLoops.Text = "Looping"; Calculator1.RunALoop(); }
前の手順で追加したコードの直後に、フォームが Calculator1 から受け取るイベントを処理するための次のコードを入力します。
private void FactorialHandler(double Value, double Calculations) // Displays the returned value in the appropriate label. { lblFactorial1.Text = Value.ToString(); // Re-enables the button so it can be used again. btnFactorial1.Enabled = true; // Updates the label that displays the total calculations performed lblTotalCalculations.Text = "TotalCalculations are " + Calculations.ToString(); } private void FactorialMinusHandler(double Value, double Calculations) { lblFactorial2.Text = Value.ToString(); btnFactorial2.Enabled = true; lblTotalCalculations.Text = "TotalCalculations are " + Calculations.ToString(); } private void AddTwoHandler(int Value, double Calculations) { lblAddTwo.Text = Value.ToString(); btnAddTwo.Enabled = true; lblTotalCalculations.Text = "TotalCalculations are " + Calculations.ToString(); } private void LoopDoneHandler(double Calculations, int Count) { btnRunLoops.Enabled = true; lblRunLoops.Text = Count.ToString(); lblTotalCalculations.Text = "TotalCalculations are " + Calculations.ToString(); }
frmCalculations のコンストラクターの } の直前に、フォームが Calculator1 から受け取るカスタム イベントを処理するコードを追加します。
Calculator1.FactorialComplete += new Calculator.FactorialCompleteHandler(this.FactorialHandler); Calculator1.FactorialMinusOneComplete += new Calculator.FactorialCompleteHandler(this.FactorialMinusHandler); Calculator1.AddTwoComplete += new Calculator.AddTwoCompleteHandler(this.AddTwoHandler); Calculator1.LoopComplete += new Calculator.LoopCompleteHandler(this.LoopDoneHandler);
アプリケーションのテスト
これで、複数の複雑な計算を実行できるフォームとコンポーネントを取り込んだプロジェクトが作成されました。マルチスレッド機能はまだ実装されていませんが、先に進む前に、まずプロジェクトをテストして機能するかどうかを確認します。
プロジェクトをテストするには
[デバッグ] メニューの [デバッグ開始] をクリックします。
アプリケーションが開始され frmCalculations が表示されます。
テキスト ボックスに「4」と入力し、[Add two] というラベルの付いたボタンをクリックします。
"6" という数字がこのラベルの下に表示され、lblTotalCalculations に "Total Calculations are 1" と表示されている必要があります。
次に [Factorial - 1] というラベルの付いたボタンをクリックします。
"6" という数字がこのボタンの下に表示され、今度は lblTotalCalculations に "Total Calculations are 4" と表示されている必要があります。
テキスト ボックスの値を「20」に変更し、[Factorial] というラベルの付いたボタンをクリックします。
ボタンの下に "2.43290200817664E+18" という数字が表示され、今度は lblTotalCalculations に "Total Calculations are 24" と表示されます。
テキスト ボックスの値を「50000」に変更し、[Run A Loop] というラベルの付いたボタンをクリックします。
このボタンが再び使用できるようになるまでには、少し時間がかかります。このボタンの下のラベルに "50000" と表示され、合計計算数が "25000024" と表示されます。
テキスト ボックスの値を「5000000」に変更し [Run A Loop] というラベルの付いたボタンをクリックし、すぐに [Add Two] というラベルの付いたボタンをクリックします。このボタンをもう一度クリックします。
ボタンおよびフォームのすべてのコントロールは、ループ処理が完了するまで応答しません。
プログラムで実行されるスレッドが 1 つだけである場合は、上の例のようなプロセッサ集中型の計算を実行すると、計算が完了するまでプログラムが拘束される傾向があります。複数のスレッドが同時に実行できるように、次のセクションでは、マルチスレッド機能をアプリケーションに追加します。
マルチスレッド機能の追加
前の例ではシングル スレッドでだけ実行されるアプリケーションの制限について説明しました。次のセクションでは、Thread クラスを使って、複数の実行スレッドをコンポーネントに追加します。
Threads サブルーチンを追加するには
コード エディターで Calculator.cs を開きます。
コードの先頭付近にクラス宣言を配置し、{ の直後に次のように入力します。
// Declares the variables you will use to hold your thread objects. public System.Threading.Thread FactorialThread; public System.Threading.Thread FactorialMinusOneThread; public System.Threading.Thread AddTwoThread; public System.Threading.Thread LoopThread;
このコード下部でクラス宣言を完結する直前に、次のメソッドを追加します。
public void ChooseThreads(int threadNumber) { // Determines which thread to start based on the value it receives. switch(threadNumber) { case 1: // Sets the thread using the AddressOf the subroutine where // the thread will start. FactorialThread = new System.Threading.Thread(new System.Threading.ThreadStart(this.Factorial)); // Starts the thread. FactorialThread.Start(); break; case 2: FactorialMinusOneThread = new System.Threading.Thread(new System.Threading.ThreadStart(this.FactorialMinusOne)); FactorialMinusOneThread.Start(); break; case 3: AddTwoThread = new System.Threading.Thread(new System.Threading.ThreadStart(this.AddTwo)); AddTwoThread.Start(); break; case 4: LoopThread = new System.Threading.Thread(new System.Threading.ThreadStart(this.RunALoop)); LoopThread.Start(); break; } }
Thread をインスタンス化するときは、ThreadStart の形式の引数が必要です。ThreadStart は、スレッドを開始するメソッドのアドレスを指すデリゲートです。ThreadStart は、パラメーターを受け取ることも値を渡すこともできないため、void メソッドを示すことしかできません。実装した ChooseThreads メソッドが、呼び出し元のプログラムから値を受け取り、この値を使用することにより、開始する必要のある適切なスレッドを決定します。
frmCalculations に適切なコードを追加するには
コード エディターで frmCalculations.cs ファイルを開き、private void btnFactorial1_Click を探します。
直接 Calculator1.Factorial1 メソッドを呼び出す行を次のようにコメント アウトします。
// Calculator1.Factorial()
Calculator1.ChooseThreads メソッドを呼び出すため、次の行を追加します。
// Passes the value 1 to Calculator1, thus directing it to start the // correct thread. Calculator1.ChooseThreads(1);
他の button_click メソッドにも同様の変更を加えます。
[!メモ]
Threads 引数の適切な値が含まれていることを確認してください。
完成したコードは次のようになります。
private void btnFactorial1_Click(object sender, System.EventArgs e) // Passes the value typed in the txtValue to Calculator.varFact1 { Calculator1.varFact1 = int.Parse(txtValue.Text); // Disables the btnFactorial1 until this calculation is complete btnFactorial1.Enabled = false; // Calculator1.Factorial(); Calculator1.ChooseThreads(1); } private void btnFactorial2_Click(object sender, System.EventArgs e) { Calculator1.varFact2 = int.Parse(txtValue.Text); btnFactorial2.Enabled = false; // Calculator1.FactorialMinusOne(); Calculator1.ChooseThreads(2); } private void btnAddTwo_Click(object sender, System.EventArgs e) { Calculator1.varAddTwo = int.Parse(txtValue.Text); btnAddTwo.Enabled = false; // Calculator1.AddTwo(); Calculator1.ChooseThreads(3); } private void btnRunLoops_Click(object sender, System.EventArgs e) { Calculator1.varLoopValue = int.Parse(txtValue.Text); btnRunLoops.Enabled = false; // Lets the user know that a loop is running lblRunLoops.Text = "Looping"; // Calculator1.RunALoop(); Calculator1.ChooseThreads(4); }
コントロールのマーシャリング呼び出し
ここでは、フォームの表示を円滑に更新できるようにします。コントロールは常にメインの実行スレッドによって所有されるため、従属スレッドからコントロールを呼び出すには "マーシャリング" 呼び出しが必要です。マーシャリングは、スレッド境界を越えて呼び出しを移動する方法で、リソースの観点から考えるとたいへんコストがかかります。マーシャリングの発生する回数を最小限にし、呼び出しをスレッド セーフな方法で行うには、Control.BeginInvoke メソッドを使用して、メインの実行スレッドでメソッドを呼び出します。これにより、スレッドの境界を越えたマーシャリングの発生を必要最小限にできます。このような呼び出しは、コントロールを操作するメソッドを呼び出すときに必要です。詳細については、「方法 : スレッドからコントロールを操作する」を参照してください。
コントロールを呼び出すプロシージャを作成するには
コード エディターで frmCalculations を開きます。宣言セクションに次のコードを追加します。
public delegate void FHandler(double Value, double Calculations); public delegate void A2Handler(int Value, double Calculations); public delegate void LDHandler(double Calculations, int Count);
Invoke と BeginInvoke では、適切なメソッドへのデリゲートが引数として必要です。これらの行では、適切なメソッドを呼び出すために BeginInvoke によって使用されるデリゲートのシグネチャを宣言します。
コードに次の空のメソッドを追加します。
public void FactHandler(double Value, double Calculations) { } public void Fact1Handler(double Value, double Calculations) { } public void Add2Handler(int Value, double Calculations) { } public void LDoneHandler(double Calculations, int Count) { }
[編集] メニューの [切り取り] と [貼り付け] を使って、FactorialHandler メソッドのすべてのコードを切り取り、FactHandler に貼り付けます。
FactorialMinusHandler と Fact1Handler、AddTwoHandler と Add2Handler、および LoopDoneHandler と LDoneHandler について前の手順を繰り返します。
終了すると、FactorialHandler、Factorial1Handler、AddTwoHandler、および LoopDoneHandler に残されたコードはなく、これらに含まれていたすべてのコードは適切な新しいメソッドに移動されています。
BeginInvoke メソッドを呼び出して、メソッドを非同期的に呼び出します。BeginInvoke は、フォーム (this) またはフォーム上の任意のコントロールから呼び出すことができます。
完了すると、コードは次のようになります。
protected void FactorialHandler(double Value, double Calculations) { // BeginInvoke causes asynchronous execution to begin at the address // specified by the delegate. Simply put, it transfers execution of // this method back to the main thread. Any parameters required by // the method contained at the delegate are wrapped in an object and // passed. this.BeginInvoke(new FHandler(FactHandler), new Object[] {Value, Calculations}); } protected void FactorialMinusHandler(double Value, double Calculations) { this.BeginInvoke(new FHandler(Fact1Handler), new Object [] {Value, Calculations}); } protected void AddTwoHandler(int Value, double Calculations) { this.BeginInvoke(new A2Handler(Add2Handler), new Object[] {Value, Calculations}); } protected void LoopDoneHandler(double Calculations, int Count) { this.BeginInvoke(new LDHandler(LDoneHandler), new Object[] {Calculations, Count}); }
一見すると、イベント ハンドラーは単純に次のメソッドを呼び出しているように見えます。実際には、イベント ハンドラーは処理のメイン スレッド上のメソッドを呼び出しています。この方法によりスレッドの境界を越えた呼び出しを減らし、ロックアップの発生を抑えながらマルチスレッド アプリケーションを効率的に実行できます。マルチスレッド環境でコントロールを使用する方法の詳細については、「方法 : スレッドからコントロールを操作する」を参照してください。
作業内容を保存します。
[デバッグ] メニューの [デバッグ開始] をクリックしてソリューションをテストします。
テキスト ボックスに「10000000」と入力し [Run A Loop] をクリックします。
このボタンの下のラベルに "Looping" と表示されます。このループの実行にはかなり時間がかかります。完了が早すぎる場合は、数字の大きさを適宜変更してください。
まだ選択可能な 3 つのボタンを連続してクリックします。すべてのボタンが入力に応答することがわかります。[Add Two] の下のラベルが最初に結果を表示します。その後、[factorial] ボタンの下のラベルに結果が表示されます。10,000,000 の階乗から返される数値は大きすぎて倍精度の変数には入りきらないため、これらの結果は無限になります。最後に少し遅れて、結果が [Run A Loop] ボタンの下に表示されます。
以上の結果から、4 つの異なる計算のセットが 4 つの異なるスレッドで同時に実行されました。ユーザー インターフェイスは入力に応答し続け、各スレッドの完了後に結果が返されました。
スレッドの調整
マルチスレッド アプリケーションを扱い慣れている方なら、入力したコードに不備があることにお気付きかもしれません。Calculator の計算を実行する各メソッドのコード行を思い出してください。
varTotalCalculations += 1;
varTotalAsOfNow = varTotalCalculations;
これらの 2 行のコードでは、パブリック変数 varTotalCalculations をインクリメントし、ローカル変数 varTotalAsOfNow をこの値に設定します。次に、この値は frmCalculations に返され、ラベル コントロールに表示されます。ところで、返された値は正しいでしょうか。シングル スレッドの実行によって動作している場合に限っては、確実に正しい値が返されます。しかし、複数のスレッドが実行されている場合は、正しい値が返されることは保証されません。各スレッドは、変数 varTotalCalculations をインクリメントできます。あるスレッドによってこの変数がインクリメントされた後、インクリメントされた値が varTotalAsOfNow にコピーされる前に、別のスレッドによってインクリメントされ、変数の値が変更されてしまう可能性があります。つまり、実際には、各スレッドが正しくない結果をレポートしている可能性があります。Visual C# には、スレッドの同期を可能にし、各スレッドが常に正確な結果を返すようにするための lock ステートメント (C# リファレンス) が用意されています。lock の構文は次のとおりです。
lock(AnObject)
{
// Insert code that affects the object.
// Insert more code that affects the object.
// Insert more code that affects the object.
// Release the lock.
}
lock ブロックが入力されると、指定された式の実行は、指定されたスレッドが対象のオブジェクトを排他ロックするまでブロックされます。上記の例では、実行は AnObject に対してブロックされます。lock は、値ではなく参照を返すオブジェクトで使用します。このようにして、実行が他のスレッドによって干渉されることなく 1 つのブロックとして続行されます。1 つの単位として実行されるステートメントのセットは、分割不可能であることになります。} を検出すると、式が解放され、スレッドは通常どおりに実行されます。
アプリケーションにロック ステートメント追加するには
コード エディターで Calculator.cs を開きます。
次のコードが記述されたすべての場所を探します。
varTotalCalculations += 1; varTotalAsOfNow = varTotalCalculations;
計算メソッドごとに 1 つずつ、このコードの 4 つのインスタンスが必要です。
コードを次のように変更します。
lock(this) { varTotalCalculations += 1; varTotalAsOfNow = varTotalCalculations; }
作業内容を保存し、前の例と同様にテストします。
プログラムのパフォーマンスにわずかな影響があることがわかります。これはコンポーネントに排他ロックがかけられるときにスレッドの実行が停止するために起こります。この方法によって、正確さは保証されますが、複数のスレッドによって得られるパフォーマンス上の利点が若干損なわれます。スレッドのロックは、必要かどうかを慎重に検討して、不可欠な場合にだけ実装してください。
参照
処理手順
チュートリアル : Visual Basic による簡単なマルチスレッド コンポーネントの作成