連載:VB 6ユーザーのための
これならマスターできるVB 2005超入門

第11回 初めてのマルチスレッドと排他制御入門

羽山 博
2007/09/11
Page1 Page2 Page3 Page4

 ではお待ちかねの、新しいスレッドを作成してそれを実行するコードに進もう。今度は、コードの前半を見てほしい。重要な部分だけ抜き出しておこう。

Imports System.Threading
        
        
        Dim myThread1 As Thread
        myThread1 = New Thread(AddressOf WithDraw)
        
        
        myThread1.Start() ' スレッドを開始する
新しいスレッドを作成し、実行するコード
  Threadクラスのオブジェクトを参照する変数myThread1を宣言。
  Threadクラスの新しいオブジェクトを作成し、myThread1で参照できるようにする。AddressOf演算子を使ってプロシージャの参照を取得している。
  ThreadクラスのStartメソッドを使ってスレッドを開始する。スレッドの処理は非同期に実行される。

 スレッドを作成・実行するにはThreadクラスを利用する。ThreadクラスはSystem.Threading名前空間にあるので、コードの最初にImportsステートメントを記述して、名前空間を指定しておけば、「System.Threading.Thread」と書かなくても、単に「Thread」と書くことができるようになる。

 スレッドの作成は、ほかのクラスのオブジェクトを作成する方法とまったく同じ。 がそのコードだ。 では、Threadクラスのオブジェクトを参照する変数myThread1を宣言し、 ではNew演算子を使ってThreadクラスの新しいオブジェクト(インスタンス)を作成している。オブジェクトの参照はmyThread1に代入されているので、このmyThread1という変数を使えば、作成された新しいスレッドの操作ができることになる。

 注意すべきはThreadコンストラクタの引数に指定した「AddressOf WithDraw」という個所。このAddressOfというのは、プロシージャの「デリゲート」を作成するというもの……といわれても、まったくイメージがわかないだろう。デリゲートとは日本語にすると「委任」といった意味になるが、その意味から考えても余計に分からなくなるのでやめておいた方がいい。

 いまはデリゲートという難しい用語にあまり惑わされず、単純に割り切って「関数の参照」とでも理解しておくといいだろう。「AddressOf WithDraw」を直訳すれば「WithDrawプロシージャのアドレス」となるので、その方が直感的に分かりやすいはずだ。

 もう少し厳密にいうと、デリゲートとは、ほかのプロシージャを間接的に呼び出すために使われるオブジェクトのことで、AddressOf演算子を使うと、そのオブジェクトの参照が得られる、ということになるのだが、要するに、そのスレッドで実行したいプロシージャの名前をAddressOfの後に書いておけばよいということだ。なお、これ以降はこの新しいスレッドのことを「WithDrawスレッド」と呼ぶことにしよう。

 スレッドの開始にはStartメソッドを使う。Startメソッドに引数を指定して、スレッドとして実行されるプロシージャにその引数を渡すこともできるが、それについては後で触れることとしよう。むしろ、ここでは、最初に述べた「正しい結果は得られない」ということを思い出してほしい。

 このプログラムでは、スレッドを開始した後、すぐにlblBalanceというラベル・コントロールのTextプロパティに新しい残高を設定している。が、残念ながら、このまま実行しても、古い残高しか表示されない。というのは、スレッドは同時に(非同期)に実行されるからだ。

 先ほどから見てきたように、引き落としを実行するWithDrawスレッドは「時間のかかる処理」であり、少なくとも5秒はかかる。従って、残高の更新が終わる前に、残高が表示されてしまうというわけだ。図3のようなイメージでとらえるといいだろう。


図3 スレッドは非同期に実行される
WithDrawスレッドの実行には5秒かかる。WithDrawスレッドがモタモタしている間に、メインのスレッドで、更新前の値がラベルに表示されてしまう。

 このような問題を解決するにはどうすればいいだろうか。簡単に思いつくのは、WithDrawスレッドの方の最後に、

lblBalance.Text = decBalance.ToString

というコードを記述し、新しい残高をラベル・コントロールに表示するというものだが、呼び出し側のコントロールなどを別のスレッド(ここではWithDrawスレッド)から利用しようとすると、スレッド間の呼び出しに関するエラーが発生してしまう。一般に、フォーム上でユーザー・インターフェイス部品として使われるコントロールはほかのスレッドからアクセスすることはできないので、残念ながらこれは却下だ。

 最も簡単な方法は、スレッドの実行が終了するのを待つという方法。その場合、WithDrawスレッドが実行されている間、メインのスレッドの実行を一時停止するという方法か、ThreadクラスのJoinメソッドを利用するという方法のいずれかが使える。

 メインのスレッドの実行を一時停止するなら、WithDrawスレッドを開始した後に、スレッドの実行状態を表すIsAliveプロパティを調べるようなループを入れればよい。

Private Sub mainProc(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles btnStart.Click

  myThread1.Start() ' スレッドを開始する

  ' 終わるのを待つ
  Do While myThread1.IsAlive
    Thread.Sleep(100)
  Loop

  ' 新しい残高を表示する
  lblBalance.Text = decBalance.ToString
End Sub
新しいスレッドの終了を待つコード
IsAliveプロパティがTrueであれば、スレッドが実行中なので、その間はSleepメソッドを使って待っているとよい

 Joinメソッドを使っても同じことができる。Joinメソッドを利用すれば、スレッドの実行が終了するまで、呼び出し側のスレッドを停止させておける。その場合は、上のコードのDo 〜 Loopの部分を、

myThread1.Join()

の1行に書き換えるだけでよい。いずれの方法でも、WithDrawスレッドの実行が終わってから、ラベルのテキストが変わるので、正しく30000と表示されるようになる*1

*1 ただし、待ち状態になるとメイン・スレッドの操作ができなくなる。それを避け、WithDrawスレッドから、呼び出し元スレッドのlblBalanceコントロールに安全に(スレッドセーフに)アクセスするには、以下のような方法を使う。このようにすれば、メインのスレッドはブロックされない。ただし、本稿の目的のレベルを大きく超えているので、方法を個条書きで示すだけにとどめる。

[ソリューションエクスプローラ]の[すべてのファイルを表示]ボタンをクリックし、[Form1.Designer.vb]のコードを表示する

クラスの定義の先頭に以下のコードを入力する。プロシージャ名は任意。

Partial Class frmMTsample
    Inherits System.Windows.Forms.Form

  Delegate Sub WithDrawCallback(ByVal data As Object) ' ←この行を入力

WithDrawプロシージャを以下のように書き換える

Private Sub WithDraw(ByVal data As Object)

  Dim decWork As Decimal
  Debug.Print("スレッド開始")
  If lblBalance.InvokeRequired Then
    decWork = decBalance ' 残高の読み出し
    decWork = decWork + decDeposit ' 預金または引き出し
    Thread.Sleep(intInterval) ' 処理にかかる時間
    decBalance = decWork ' 残高の書き出し
    Dim d = New WithDrawCallback(AddressOf WithDraw)
    lblBalance.Invoke(d, New Object() {decBalance.ToString})
  Else
    lblBalance.Text = decBalance.ToString
    Debug.Print("スレッド終了")
  End If
End Sub

  詳しくは、「.NET TIPS:Windowsフォームで別スレッドからコントロールを操作するには?」などを参考にしていただきたい。

サンプル・プログラム16の拡張 − 作成したスレッドにデータを渡す

 スレッド間でデータをやりとりする方法にもいくつかの方法があるが、ここでは最も簡単な方法を示しておこう。

 ThreadクラスのStartメソッドには、Object型の変数を引数として指定することができ、スレッドとして実行されるプロシージャでは、その値を引数として受け取ることができる。

 例えば、サンプル・プログラム16では、預入額(または引出額)をフォーム・レベルの変数にして、いずれのプロシージャからも利用しているが、WithDrawプロシージャの中でこの値を変更することは普通あり得ないので、これを引数として渡せるようにしてみよう。コードは以下のようになる。

Imports System.Threading

Public Class frmMTsample

  Private decBalance As Decimal = 50000D ' 最初の残高
  Private intInterval As Integer ' 処理にかかる時間

  Private Sub mainProc(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles btnStart.Click

    Dim decDeposit As Decimal ' 預入額または引出額
    Dim myThread1 As Thread
    myThread1 = New Thread(AddressOf WithDraw)
    '
    intInterval = 5000 ' 5秒かかるものとする
    decDeposit = -20000D ' 20000円引き落とすものとする
    myThread1.Start(decDeposit) ' スレッドを開始する
    myThread1.Join() ' スレッドの終了を待つ

    ' 新しい残高を表示する
    lblBalance.Text = decBalance.ToString

  End Sub

  Private Sub WithDraw(ByVal data As Object)

    Dim decWork As Decimal

    Debug.Print("スレッド開始")
    decWork = decBalance  残高の読み出し
    decWork = decWork + CType(data, Decimal) ' 預金または引き出し

    Thread.Sleep(intInterval) ' 処理にかかる時間
    decBalance = decWork ' 残高の書き出し
    Debug.Print("スレッド終了")

  End Sub
End Class
新しいスレッドを実行するときにデータを渡すコード
  変数decDepositをローカルに宣言する。
  ThreadクラスのStartメソッドの引数としてdecDepositを渡す。
  WithDrawプロシージャの引数を書き換える。値渡し(ByVal)で、データ型はObjectにしておく必要がある。
  CType関数を使って元のDecimal型に変換する。

 変更したコードは4カ所だけ。変数decDepositは引数として渡されるので、フォーム・レベルの変数にしておく必要はない。そこで、 のようにmainProcプロシージャのローカルな変数にしておいた。スレッドを開始するときに、 で、decDepositを新しいスレッドに渡していることが分かるだろう。

  ではWithDrawプロシージャの引数を書き換えている。これまで、引数はなかったが、値渡しのObject型の引数を1つ記述しておく。引数の名前は任意だが、参照渡しにしたり、ほかのデータ型を指定するとエラーになることに注意。最後の では、渡された値をDecimal型に変換し、残高を計算している。渡された値はObject型なので、CType関数によるデータ型の変換が必要になるというわけだ。


 INDEX
  連載:VB 6ユーザーのための<BR>これならマスターできるVB 2005超入門
  第11回 初めてのマルチスレッドと排他制御入門
    1.サンプル・プログラム16 − 初めてのマルチスレッド・プログラム(1)
  2.サンプル・プログラム16 − 初めてのマルチスレッド・プログラム(2)
    3.サンプル・プログラム17 − Mutexによる排他制御
    4.Mutexを利用した排他制御を組み込む
 
インデックス・ページヘ  「これならマスターできるVB 2005超入門」


Insider.NET フォーラム 新着記事
  • 第2回 簡潔なコーディングのために (2017/7/26)
     ラムダ式で記述できるメンバの増加、throw式、out変数、タプルなど、C# 7には以前よりもコードを簡潔に記述できるような機能が導入されている
  • 第1回 Visual Studio Codeデバッグの基礎知識 (2017/7/21)
     Node.jsプログラムをデバッグしながら、Visual Studio Codeに統合されているデバッグ機能の基本の「キ」をマスターしよう
  • 第1回 明瞭なコーディングのために (2017/7/19)
     C# 7で追加された新機能の中から、「数値リテラル構文の改善」と「ローカル関数」を紹介する。これらは分かりやすいコードを記述するのに使える
  • Presentation Translator (2017/7/18)
     Presentation TranslatorはPowerPoint用のアドイン。プレゼンテーション時の字幕の付加や、多言語での質疑応答、スライドの翻訳を行える
@ITメールマガジン 新着情報やスタッフのコラムがメールで届きます(無料)
- PR -

注目のテーマ

業務アプリInsider 記事ランキング

本日 月間
ソリューションFLASH