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で判断しているけど、やはり無矛盾な一貫した状態にしておきたい。うむ、これで安心して外出できる。いってきます。