【実務・中級編】【上級】フォームの「TimerInterval」と「OnTimer」で実現する、バックグラウンドでの擬似マルチスレッド処理 – Access VBA解析バイブル

スポンサーリンク

【上級】Accessのシングルスレッド制約を打ち破る:TimerIntervalとOnTimerによる擬似非同期バックグラウンド処理

こんにちは。開発プロジェクトの現場において、数々のレガシーなAccessシステムをモダンで堅牢な基幹へと生まれ変わらせてきたチーフアーキテクトだ。

Access開発において、避けて通れない最大の壁が「シングルスレッド制約」である。
巨大なCSVのインポート、数万件のレコードを一括処理する複雑なクエリの実行、あるいは外部APIへのリクエスト。これらを一つのプロシージャで直列に実行した瞬間、AccessのUIは完全に沈黙する。「応答なし」の冷酷なダイアログを見つめながら、祈るような気持ちでプログレスバーの完了を待った経験が、あなたにも一度や二度ではないはずだ。

「Accessだから仕方ない」と諦めるのはまだ早い。
今回は、フォームの `TimerInterval` プロパティと `OnTimer` イベントを極限までチューニングし、「UIをフリーズさせずに重い処理を小分けに実行する(=擬似マルチスレッド化)」ための実践的なアーキテクチャを伝授する。

なぜ「DoEvents」では不十分なのか?

愚直な開発者は、ループ処理の中に `DoEvents` を挟む。

‘ 【アンチパターン】絶対にやってはいけない実装
For i = 1 to 100000
‘ 重い処理
If i Mod 100 = 0 Then DoEvents ‘ UIの固まりを防ごうとする悪あがき
Next i

このアプローチは百害あって一利なしだ。
`DoEvents` はOSに制御を一時的に返すものの、イベントキューの処理コストが異常に高く、処理全体のパフォーマンスが数倍〜数十倍に低下する。さらに最悪なのは、ユーザーがフォーム上の別のボタンを連打した際に「再入可能性(Reentrancy)の問題」を引き起こし、メモリリークや予期せぬクラッシュ(致命的なアプリケーションエラー)の温床となることだ。

プロが選ぶべき道は、`DoEvents` ではなく、「タイマーイベントによるタイムライスイシング(時分割処理)」である。

アーキテクチャの全体像:ステートマシン(状態機械)の構築

バックグラウンドで安全に処理を分割・実行するためには、フォームを一種の「ステートマシン(状態機械)」として設計する必要がある。

1. 初期化フェーズ: 処理対象のデータを一時テーブルやコレクションに保持し、ポインタ(現在の処理位置)をリセットする。
2. タイマー駆動フェーズ: `TimerInterval`(例: 100ms)ごとに `OnTimer` イベントを発火させ、許容されたチャンク(小分けサイズ)だけ処理を進める。
3. 終了・例外フェーズ: 全処理が完了した、あるいはエラーが発生した時点でタイマーを停止し、UIを通常状態に戻す。

この設計により、Accessは常にユーザーからの入力イベントを受け付ける余裕(UIスレッドの解放)を保ちながら、裏で着実に重い処理を消化していく。

【プロダクションコード】堅牢な非同期処理フォームの実装

実際の現場でそのまま組み込める、モジュールレベル変数で状態を厳密に管理するクラス・フォームモジュールの実装例を示す。

ここでは、「巨大な外部テキストファイルを1行ずつパースしてテーブルに流し込む」という、よくある重い処理を想定しよう。

Option Compare Database
Option Explicit

‘ =========================================================================
‘ モジュールレベル変数(状態管理のキーストーン)
‘ =========================================================================
Private m_IsProcessing As Boolean ‘ 処理中フラグ(二重起動防止)
Private m_FileNumber As Integer ‘ 読み込み用ファイル番号
Private m_TargetFilePath As String ‘ 処理対象ファイルパス
Private m_TotalLines As Long ‘ 総行数(進捗計算用)
Private m_ProcessedLines As Long ‘ 処理済み行数
Private Const CHUNK_SIZE As Long = 500 ‘ 1回のタイマーイベントで処理する行数

‘ ————————————————————————-
‘ イベント: 非同期処理開始ボタン
‘ ————————————————————————-
Private Sub btnStartAsync_Click()
On Error GoTo ErrorHandler

‘ 1. 二重起動の厳重なガード
If m_IsProcessing Then
MsgBox “現在バックグラウンドで処理を実行中です。完了までお待ちください。”, vbExclamation, “排他制御”
Exit Sub
End If

m_TargetFilePath = “C:\Data\HugeImportData.csv”
If Dir(m_TargetFilePath) = “” Then
MsgBox “対象ファイルが見つかりません。”, vbCritical
Exit Sub
End If

‘ 2. 状態の初期化
m_IsProcessing = True
m_ProcessedLines = 0
m_TotalLines = CountFileLines(m_TargetFilePath) ‘ 行数カウント(軽量な事前スキャン)

‘ ファイルを開く
m_FileNumber = FreeFile
Open m_TargetFilePath For Input As #m_FileNumber

‘ 3. UIのロック(操作制限)と進捗バーの初期化
Me.btnStartAsync.Enabled = False
Me.lblStatus.Caption = “バックグラウンド処理を実行中…”
Me.ProgressBar.Width = 0

‘ 4. タイマーの起動(ここで初めてTimerIntervalを設定する)
‘ 100ミリ秒ごとにOnTimerイベントをトリガー
Me.TimerInterval = 100

Exit Sub

ErrorHandler:
Call TerminateProcess(True)
MsgBox “初期化エラー: ” & Err.Description, vbCritical
End Sub

‘ ————————————————————————-
‘ イベント: タイマーイベント(心臓部)
‘ ————————————————————————-
Private Sub Form_Timer()
Dim i As Long
Dim lineData As String
Dim db As DAO.Database
Dim rs As DAO.Recordset

‘ タイマー多重発火を防ぐためのガード
Me.TimerInterval = 0

On Error GoTo TimerErrorHandler

Set db = CurrentDb
‘ トランザクション処理はチャンク単位で行うのがパフォーマンスと整合性のバランスが良い
Set rs = db.OpenRecordset(“T_ImportLog”, dbOpenDynaset)

‘ チャンクサイズ分だけループを回す
For i = 1 To CHUNK_SIZE
If EOF(m_FileNumber) Then
‘ 2. 終了判定
Call TerminateProcess(False)
MsgBox “バックグラウンド処理が正常に完了しました。”, vbInformation, “完了”
Exit Sub
End If

Line Input #m_FileNumber, lineData
m_ProcessedLines = m_ProcessedLines + 1

‘ — ここに実際の重いパース・DB書き込み処理を記述 —
rs.AddNew
rs!LogData = lineData
rs!ImportDate = Now()
rs.Update
‘ —————————————————-
Next i

‘ UIのプログレス更新(フリーズしない快適な描画)
Call UpdateProgressUI

‘ 処理が継続中の場合のみ、タイマーを再設定して次のチャンクへ
If m_IsProcessing Then
Me.TimerInterval = 50 ‘ 50msのインターバルで次のブロックへ
End If

rs.Close
Set rs = Nothing
Set db = Nothing
Exit Sub

TimerErrorHandler:
If Not rs Is Nothing Then rs.Close: Set rs = Nothing
Set db = Nothing
Call TerminateProcess(True)
MsgBox “タイマー処理中に致命的なエラーが発生しました: ” & Err.Description, vbCritical
End Sub

‘ ————————————————————————-
‘ 内部メソッド: 処理のクリーンアップ(終了・異常終了共通)
‘ ————————————————————————-
Private Sub TerminateProcess(ByVal isError As Boolean)
On Error Resume Next

‘ ファイルクローズ
If m_FileNumber > 0 Then
Close #m_FileNumber
m_FileNumber = 0
End If

‘ 状態リセット
m_IsProcessing = False
Me.TimerInterval = 0 ‘ タイマー完全停止

‘ UIの復元
Me.btnStartAsync.Enabled = True
If isError Then
Me.lblStatus.Caption = “処理が中断されました。”
Else
Me.lblStatus.Caption = “待機中”
Me.ProgressBar.Width = 3000 ‘ 完了時の最大幅(デザインに合わせて調整)
End If
End Sub

‘ ————————————————————————-
‘ 内部メソッド: 進捗率のグラフィック表現
‘ ————————————————————————-
Private Sub UpdateProgressUI()
On Error Resume Next
If m_TotalLines <= 0 Then Exit Sub Dim ratio As Double ratio = CDbl(m_ProcessedLines) / CDbl(m_TotalLines) If ratio > 1# Then ratio = 1#

‘ プログレスバーの幅を動的に変更(最大幅3000twipと仮定)
Me.ProgressBar.Width = 3000 ratio
Me.lblStatus.Caption = “処理中… (” & m_ProcessedLines & ” / ” & m_TotalLines & ” 行)”
End Sub

‘ ————————————————————————-
‘ 内部メソッド: 事前行数カウント
‘ ————————————————————————-
Private Function CountFileLines(ByVal filePath As String) As Long
Dim fNum As Integer
Dim cnt As Long
Dim dummy As String

fNum = FreeFile
Open filePath For Input As #fNum
cnt = 0
Do While Not EOF(fNum)
Line Input #fNum, dummy
cnt = cnt + 1
Loop
Close #fNum
CountFileLines = cnt
End Function

プロダクション環境における極限の注意点

この設計を実際の業務システムに導入する際、シニアエンジニアとして絶対に押さえておかなければならない「罠」がいくつか存在する。

1. タイマーの再入防止(Reentrancy Guard)

タイマーイベントの処理中に、もし何らかの理由で次の `OnTimer` が発火してしまうと、Accessはメモリ上でパニックを起こす。
そのため、コードの冒頭で `Me.TimerInterval = 0` を実行し、処理が完全に終わるまで次のタイマーを止める防壁を張る(上記コード参照)。これが破綻のない堅牢な非同期処理の命綱となる。

2. エラーハンドリングとリソース解放

バックグラウンド処理中に予期せぬエラー(ネットワーク切断、ファイルロック、DBの排他エラーなど)が発生した場合、ファイルハンドルやDAO/ADOのレコードセットが開きっぱなしになる。
必ず `TerminateProcess` のようなクリーンアップ専用のサブルーチンを集約し、異常系でも確実にリソースが解放される構造にすること。

3. DAO vs ADO とバックグラウンド処理

Accessのフロントエンドにおいて、ローカルテーブルやリンクテーブルへの高速なデータ操作には `CurrentDb.OpenRecordset`(DAO)を使うのが最もオーバヘッドが少ない。
もしバックグラウンドで外部SQL Server等と通信する場合は、非同期接続(Connectionストリングの拡張設定など)の検討が必要になるが、Accessフォームのスコープ内であれば、上記のように「DAOによるチャンク単位のトランザクション制御」を行うのが最も安定し、かつ開発コスト対効果が高い。

チーフアーキテクトからの総括

「ボタンを押したら画面が固まり、終わるまで何もできない」――そんな前時代のAccessアプリケーションの常識は、今日この瞬間からあなたの手で覆すべきだ。

`TimerInterval` と `OnTimer` を駆使したステートマシン設計は、コードの行数こそ少し増えるものの、「ユーザーエクスペリエンス(UX)の向上」「CPU負荷の平準化」「予期せぬクラッシュの回避」という、プロフェッショナルなシステムに求められるすべての要件を満たしてくれる。

真に信頼される業務自動化ツールとは、裏側の泥臭い処理を完璧に隠蔽し、表側では常にユーザービリティを最優先し続けるエレガントさを持っていなければならない。
ぜひ、あなたの開発現場のレガシーなフォームにこの知見を組み込み、圧倒的なパフォーマンスの差を体感してほしい。

タイトルとURLをコピーしました