Visual Studio 2008でマルチスレッドのテスト

前のエントリでは悩んだマルチスレッドですが、VB2008を入れてみたのでちょっと味見にマルチスレッドのテストPを作り中。フォームコントロールにBackgroundWorkerなんてコントロールがあって、お手軽に作れちゃったりするみたいだけど、Threadオブジェクトを使う方法でひとまずトライだ。

で、2つのスレッドを起動、終了させるボタンを作ったりしてフォーム作っていたんだけど、フォントがUIゴシックで、「なんでメイリオじゃないんだ!」と思って変更。そしたら、そのフォームに属しているコンポーネント(テキストラベルとボタン)のフォントが全部変わった! でもってボタンの大きさも変わるし、フォーム全体の大きさも、今までの比を維持したままズルっと大きくなった。

使う場面によっては便利だったり困ったりするだろうけど、こーゆー単純なケースでは便利だ。ベクターグラフィックスの世界を、ちょっと感じたりしました。

…うーん、Threadのコンストラクタに渡した手続きからフォームのコントロールの値を操作しようとしたらエラーだ。なんか、Threadオブジェクトからフォームのコントロールさわんなよ!とかMSDNに書いてあるし。しょうがない、BackgroundWorker使って楽をしよう。

うし、できた。でも長いから折りたたんでおこうか。

Imports System.ComponentModel
Imports System.Threading.Thread

Public Class MainForm
    Dim numberForBackgroundWorker As Long = 0
    Const goalForBackgroundWorker = 600
    Dim numberForTimer As Long = 0
    Const goalForTimer = 600

    'BackgroundWorker非同期実行処理のエントリポイント
    Private Sub BackgroundWorker1_DoWork( _
                      ByVal sender As System.Object _
                    , ByVal e As System.ComponentModel.DoWorkEventArgs _
                    ) Handles BackgroundWorker1.DoWork
        'キックされたBackgroundWorker本体はsenderパラメータでもらえる。
        'CancelAsyncによるキャンセル要求をチェックするのにこれを使う。
        Dim worker As BackgroundWorker = CType(sender, BackgroundWorker)
        'バカっぽいけど、お風呂で体があったまるまで、数を数えてみる。
        '「もうあがんなさーい」と言われたらおしまい。
        Do While (numberForBackgroundWorker < goalForBackgroundWorker) And (Not e.Cancel)
            If worker.CancellationPending Then
                e.Cancel = True
            End If
            numberForBackgroundWorker += 1
            worker.ReportProgress(numberForBackgroundWorker * 100 / goalForBackgroundWorker)
            e.Result = numberForBackgroundWorker
            Sleep(100)
        Loop
        e.Result = "Normal end."
    End Sub

    'BackgroundWorkerの進捗報告ハンドラ
    Private Sub backgroundWorker1_ProgressChanged( _
                      ByVal sender As Object _
                    , ByVal e As ProgressChangedEventArgs _
                    ) Handles BackgroundWorker1.ProgressChanged
        'フォームのテキストラベルに進捗状況をセット。
        'このハンドラからならフォームのコンポーネントを操作しても怒られない。
        'スレッドセーフにコントロールの操作を行わせる為の仕組み、ということか。
        BackgroundMonitor.Text = e.ProgressPercentage
    End Sub

    'BackgroundWorkerの完了通知ハンドラ
    Private Sub backgroundWorker1_RunWorkerCompleted( _
                      ByVal sender As Object _
                    , ByVal e As RunWorkerCompletedEventArgs _
                    ) Handles BackgroundWorker1.RunWorkerCompleted
        '例外Throwされていないかチェック
        If Not (e.Error Is Nothing) Then
            MessageBox.Show(e.Error.Message)
        ElseIf e.Cancelled Then
            'キャンセルされていたら、その旨表示。
            BackgroundMonitor.Text = "Cancelled"
        Else
            'キャンセルされていなければ、正常終了。
            '結果はe.Resultプロパティにセットされている。
            'モノはObject型なので、なんでもアリ。
            BackgroundMonitor.Text = e.Result.ToString()
        End If
    End Sub

    '非同期処理の開始要求ボタン
    Private Sub StartBackground_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles StartBackground.Click
        numberForBackgroundWorker = 0
        BackgroundWorker1.RunWorkerAsync()
    End Sub

    '非同期処理のキャンセル要求ボタン
    Private Sub CancelBackground_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles CancelBackground.Click
        BackgroundWorker1.CancelAsync()
    End Sub

    'タイマー処理のエントリポイント
    Private Sub Timer1_Tick(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles Timer1.Tick
        'ゴールまで来たら、コントロール停止と終了の旨表示。
        If (numberForTimer >= goalForTimer) Then
            Timer1.Enabled = False
            TimerMonitor.Text = "Normal end."
        Else
            'ゴールまで来ていないなら、数を数える。
            numberForTimer += 1
            TimerMonitor.Text = Int(numberForTimer * 100 / goalForTimer)
        End If
    End Sub

    'タイマー処理の開始要求ボタン
    Private Sub StartTimer_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles StartTimer.Click
        numberForTimer = 0
        Timer1.Enabled = True
    End Sub

    'タイマー処理のキャンセル要求ボタン
    Private Sub TerminateThread2_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles CancelTimer.Click
        'コントロール停止とキャンセルされた旨表示。
        Timer1.Enabled = False
        TimerMonitor.Text = "Cancelled"
    End Sub
End Class

フォーム定義はこんなの。

 _
Partial Class MainForm
    Inherits System.Windows.Forms.Form

    'フォームがコンポーネントの一覧をクリーンアップするために dispose をオーバーライドします。
     _
    Protected Overrides Sub Dispose(ByVal disposing As Boolean)
        Try
            If disposing AndAlso components IsNot Nothing Then
                components.Dispose()
            End If
        Finally
            MyBase.Dispose(disposing)
        End Try
    End Sub

    'Windows フォーム デザイナで必要です。
    Private components As System.ComponentModel.IContainer

    'メモ: 以下のプロシージャは Windows フォーム デザイナで必要です。
    'Windows フォーム デザイナを使用して変更できます。  
    'コード エディタを使って変更しないでください。
     _
    Private Sub InitializeComponent()
        Me.components = New System.ComponentModel.Container
        Me.StartBackground = New System.Windows.Forms.Button
        Me.BackgroundMonitor = New System.Windows.Forms.Label
        Me.TimerMonitor = New System.Windows.Forms.Label
        Me.StartTimer = New System.Windows.Forms.Button
        Me.CancelTimer = New System.Windows.Forms.Button
        Me.CancelBackground = New System.Windows.Forms.Button
        Me.BackgroundWorker1 = New System.ComponentModel.BackgroundWorker
        Me.Timer1 = New System.Windows.Forms.Timer(Me.components)
        Me.SuspendLayout()
        '
        'StartBackground
        '
        Me.StartBackground.Location = New System.Drawing.Point(24, 128)
        Me.StartBackground.Margin = New System.Windows.Forms.Padding(3, 4, 3, 4)
        Me.StartBackground.Name = "StartBackground"
        Me.StartBackground.Size = New System.Drawing.Size(125, 34)
        Me.StartBackground.TabIndex = 0
        Me.StartBackground.Text = "StartBackground"
        Me.StartBackground.UseVisualStyleBackColor = True
        '
        'BackgroundMonitor
        '
        Me.BackgroundMonitor.AutoSize = True
        Me.BackgroundMonitor.Location = New System.Drawing.Point(22, 46)
        Me.BackgroundMonitor.Name = "BackgroundMonitor"
        Me.BackgroundMonitor.Size = New System.Drawing.Size(46, 18)
        Me.BackgroundMonitor.TabIndex = 1
        Me.BackgroundMonitor.Text = "Label1"
        '
        'TimerMonitor
        '
        Me.TimerMonitor.AutoSize = True
        Me.TimerMonitor.Location = New System.Drawing.Point(163, 46)
        Me.TimerMonitor.Name = "TimerMonitor"
        Me.TimerMonitor.Size = New System.Drawing.Size(46, 18)
        Me.TimerMonitor.TabIndex = 2
        Me.TimerMonitor.Text = "Label2"
        '
        'StartTimer
        '
        Me.StartTimer.Location = New System.Drawing.Point(166, 128)
        Me.StartTimer.Margin = New System.Windows.Forms.Padding(3, 4, 3, 4)
        Me.StartTimer.Name = "StartTimer"
        Me.StartTimer.Size = New System.Drawing.Size(125, 34)
        Me.StartTimer.TabIndex = 3
        Me.StartTimer.Text = "StartTimer"
        Me.StartTimer.UseVisualStyleBackColor = True
        '
        'CancelTimer
        '
        Me.CancelTimer.Location = New System.Drawing.Point(166, 171)
        Me.CancelTimer.Margin = New System.Windows.Forms.Padding(3, 4, 3, 4)
        Me.CancelTimer.Name = "CancelTimer"
        Me.CancelTimer.Size = New System.Drawing.Size(125, 34)
        Me.CancelTimer.TabIndex = 5
        Me.CancelTimer.Text = "CancelTimer"
        Me.CancelTimer.UseVisualStyleBackColor = True
        '
        'CancelBackground
        '
        Me.CancelBackground.Location = New System.Drawing.Point(24, 171)
        Me.CancelBackground.Margin = New System.Windows.Forms.Padding(3, 4, 3, 4)
        Me.CancelBackground.Name = "CancelBackground"
        Me.CancelBackground.Size = New System.Drawing.Size(125, 34)
        Me.CancelBackground.TabIndex = 4
        Me.CancelBackground.Text = "CancelBackground"
        Me.CancelBackground.UseVisualStyleBackColor = True
        '
        'BackgroundWorker1
        '
        Me.BackgroundWorker1.WorkerReportsProgress = True
        Me.BackgroundWorker1.WorkerSupportsCancellation = True
        '
        'Timer1
        '
        '
        'MainForm
        '
        Me.AutoScaleDimensions = New System.Drawing.SizeF(7.0!, 18.0!)
        Me.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font
        Me.ClientSize = New System.Drawing.Size(321, 227)
        Me.Controls.Add(Me.CancelTimer)
        Me.Controls.Add(Me.CancelBackground)
        Me.Controls.Add(Me.StartTimer)
        Me.Controls.Add(Me.TimerMonitor)
        Me.Controls.Add(Me.BackgroundMonitor)
        Me.Controls.Add(Me.StartBackground)
        Me.Font = New System.Drawing.Font("メイリオ", 9.0!, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point, CType(128, Byte))
        Me.Margin = New System.Windows.Forms.Padding(3, 4, 3, 4)
        Me.Name = "MainForm"
        Me.Text = "MultiThreadTest "
        Me.ResumeLayout(False)
        Me.PerformLayout()

    End Sub
    Friend WithEvents StartBackground As System.Windows.Forms.Button
    Friend WithEvents BackgroundMonitor As System.Windows.Forms.Label
    Friend WithEvents TimerMonitor As System.Windows.Forms.Label
    Friend WithEvents StartTimer As System.Windows.Forms.Button
    Friend WithEvents CancelTimer As System.Windows.Forms.Button
    Friend WithEvents CancelBackground As System.Windows.Forms.Button
    Friend WithEvents BackgroundWorker1 As System.ComponentModel.BackgroundWorker
    Friend WithEvents Timer1 As System.Windows.Forms.Timer

End Class

簡単じゃん! マルチスレッド簡単じゃん!!! .NET Framework万歳!! ハイルゲイツ!!!

…ちょっと気になって非同期処理のDoループの記述をちょっと変えてみる。

    'BackgroundWorker非同期実行処理のエントリポイント
    Private Sub BackgroundWorker1_DoWork( _
                      ByVal sender As System.Object _
                    , ByVal e As System.ComponentModel.DoWorkEventArgs _
                    ) Handles BackgroundWorker1.DoWork
        'キックされたBackgroundWorker本体はsenderパラメータでもらえる。
        'CancelAsyncによるキャンセル要求をチェックするのにこれを使う。
        Dim worker As BackgroundWorker = CType(sender, BackgroundWorker)
        'バカっぽいけど、お風呂で体があったまるまで、数を数えてみる。
        '「もうあがんなさーい」と言われたらおしまい。
        Do While (numberForBackgroundWorker < goalForBackgroundWorker)
            If worker.CancellationPending Then
                e.Cancel = True
                Exit Sub
            End If
            numberForBackgroundWorker += 1
            worker.ReportProgress(numberForBackgroundWorker * 100 / goalForBackgroundWorker)
            e.Result = numberForBackgroundWorker
            Sleep(100)
        Loop
        e.Result = "Normal end."
    End Sub

こっちの方が直接的というか、わかりやすいかな。Whileの条件が短くて済むし。CancellationPendingの判断の後、Exit DoになっていたのをExit Subに変更。Exit Doだと、e.Resultが"Normal end."になってしまう。イカンイカン。
んー、でもExit Doにしてe.Cancelをチェックしてe.Resultをセットする方がいいかな。このあたりは「絶対にこう!」っていうのは決められない気がする。Exit Subをあちこちでされると、手続きの最後の後始末とかを統一してさせたいときに例外ができちゃうとか。まあ、ポリシー次第か。

…だめだ。外出しようと思ったけど、気持ちがわるくてしょうがない。やっぱりExit Doして最後はこうだ!

        '後始末は最後でまとめてやります。
        If e.Cancel Then
            e.Result = "Cancelled."
        Else
            e.Result = "Normal end."
        End If

キャンセルされたかどうかはe.Cancelledで判断しているけど、やはり無矛盾な一貫した状態にしておきたい。うむ、これで安心して外出できる。いってきます。