Access VBAを掌握する極限の知見:Form.TimerIntervalによる「擬似マルチスレッド」の極意
VBAはシングルスレッドで動作する。これがこの言語の宿命であり、GUIとバックグラウンド処理を同一空間で共存させようとした瞬間に開発者を絶望の淵に突き落とす最大の障壁だ。
数万件のレコードを処理するループ、外部APIへの同期リクエスト、巨大なCSVファイルのパース。これらをメインスレッドで実行すれば、Accessの画面は即座に「応答なし」と化し、OSは容赦なくウィンドウを白濁させる。ユーザーは苛立ち、タスクマネージャーから強制終了という名の死刑宣告を下す。
しかし、シニアエンジニアであれば知っているはずだ。Accessには、このシングルスレッドの呪縛を巧みにすり抜け、非同期処理を模倣する強力な武器が備わっていることを。それがフォームの `TimerInterval` プロパティと `Timer` イベントである。
本稿では、単なるタイマー処理の入門ではない。Windows APIの呼吸を感じ取り、メモリリークとオブジェクトの寿命を完全に支配した者だけが到達できる、極限の「擬似マルチスレッド」アーキテクチャを解ートする。
—
1. なぜ `TimerInterval` なのか? アーキテクチャの根本思想
Accessにおける非同期処理の実現アプローチは大きく分けて二つある。
1. `WScript.Shell` や `VBScript.RegExp` などを駆使した別プロセス(WSH/PowerShell)の非同期キック。
2. フォームの `Timer` イベントを利用したタイムスラッシング(時分割処理)。
プロセス間通信(IPC)や環境依存のセキュリティポリシーに悩まされる前者に対し、`TimerInterval` は 同一メインスレッドのメッセージループの隙間を縫ってコード片を断続的に実行する。
[メインスレッドのタイムライン]
—[UI描画]—[Timerイベント(チャンク処理)]—[UI描画]—[Timerイベント(チャンク処理)]—>
数秒かかる重い処理を「1回あたり50件のチャンク(塊)」に分割し、数ミリ秒おきのタイマーで順次消化していく。これにより、UIの描画スレッド(メッセージポンプ)が常に解放され、ユーザーは処理中であってもフォームの移動やキャンセルボタンの押下が可能になる。
—
2. 実装:破綻なき非同期バックグラウンド処理エンジン
以下に、実業務のシステム間連携(ログ監視・APIポーリング等)を想定した、プロダクション品質のコードを示す。
このコードは以下の要件を満たしている。
- 再入防止(Reentrancy Guard): タイマー処理中に次のタイマーイベントが発火してスタックオーバーフローや競合を起こすのを防ぐ。
- 完全なオブジェクト解放: `CurrentDb` やレコードセットの放置によるメモリリークの根絶。
- UIの滑らかさの維持: 処理を細分化し、CPUを独占しない。
フォームモジュール:`frmBackgroundWorker`
Option Compare Database
Option Explicit
‘ ==============================================================================
‘ 担当アーキテクト特製:非同期タイマー制御エンジン
‘ ==============================================================================
‘ 状態管理フラグ(再入防止の要)
Private m_IsProcessing As Boolean
Private m_TaskQueue As Collection
‘ 処理進捗を保持するカウンター
Private m_CurrentOffset As Long
Private Const CHUNK_SIZE As Long = 100 ‘ 1回あたりの処理件数
Private Sub Form_Load()
‘ キューの初期化
Set m_TaskQueue = New Collection
m_IsProcessing = False
m_CurrentOffset = 0
‘ タイマーを無効状態で待機 (0 = 停止)
Me.TimerInterval = 0
End Sub
Private Sub Form_Unload(Cancel As Integer)
‘ 終了時のクリーンアップ
Set m_TaskQueue = Nothing
End Sub
‘ 외부からタスクを開始するためのパブリックメソッド
Public Sub StartBackgroundProcess()
If m_IsProcessing Then Exit Sub
‘ ここで初期のワークロードをキューイングするなどの前処理
m_CurrentOffset = 0
‘ タイマー間隔を 200ms に設定して駆動開始
Me.TimerInterval = 200
‘ ステータス表示の更新
Me.lblStatus.Caption = “バックグラウンド処理実行中…”
Me.lblStatus.ForeColor = RGB(0, 128, 0)
End Sub
Private Sub Form_Timer()
‘ 再入防止:前回のタイマー処理がまだ終わっていなければ即座に抜ける
If m_IsProcessing Then Exit Sub
‘ 処理中フラグを立てる(セマフォの代用)
m_IsProcessing = True
On Error GoTo ErrorHandler
‘ ————————————————————————–
‘ バックグラウンド処理の本体(チャンク単位で実行)
‘ ————————————————————————–
Dim wsProcessed As Boolean
wsProcessed = ProcessChunkWorkload()
If Not wsProcessed then
‘ 全処理完了時のクリーンアップ
Me.TimerInterval = 0
Me.lblStatus.Caption = “待機中(処理完了)”
Me.lblStatus.ForeColor = RGB(0, 0, 0)
MsgBox “バックグラウンド処理が正常に完了しました。”, vbInformation, “システム通知”
End If
ErrorHandler_Exit:
‘ 確実にフラグを下ろす。これを怠ると二度とタイマーが動かなくなる。
m_IsProcessing = False
Exit Sub
ErrorHandler:
‘ 致命的なエラーの捕捉
Me.TimerInterval = 0
m_IsProcessing = False
MsgBox “バックグラウンド処理でエラーが発生しました: ” & Err.Description, vbCritical, “致命的エラー”
Resume ErrorHandler_Exit
End Sub
Private Function ProcessChunkWorkload() As Boolean
‘ 【重要】CurrentDbを変数に保持し、メソッドスコープを抜ける際に確実に解放する
Dim db As DAO.Database
Dim rs As DAO.Recordset
Dim processedCount As Long
Set db = CurrentDb()
‘ 例:未処理のログデータをバッチ処理で外部APIへ送信する等のシミュレーション
‘ パフォーマンス最適化のため、必要な分だけレコードセットを開く
Set rs = db.OpenRecordset( _
“SELECT TOP ” & CHUNK_SIZE & ” ID, Payload, Status FROM T_IntegrationQueue WHERE Status = 0 ORDER BY ID”, _
dbOpenSnapshot)
If rs.EOF Then
‘ 処理すべきデータが存在しない
rs.Close
Set rs = Nothing
Set db = Nothing
ProcessChunkWorkload = False
Exit Function
End If
processedCount = 0
‘ トランザクションの粒度をチャンク単位に合わせる
db.Execute “BEGIN TRANSACTION”, dbFailOnError
Do While Not rs.EOF
‘ ———————————————————————-
‘ ここに実際の重い処理(API通信やファイルI/O)を記述
‘ ———————————————————————-
‘ 例: 簡易的にステータスを更新するクエリを実行
db.Execute “UPDATE T_IntegrationQueue SET Status = 1 WHERE ID = ” & rs!ID, dbFailOnError
processedCount = processedCount + 1
rs.MoveNext
Loop
db.Execute “COMMIT”, dbFailOnError
‘ オブジェクトの明示的解放(メモリリークの完全排除)
rs.Close
Set rs = Nothing
Set db = Nothing
‘ まだデータが残っている可能性を示すためにTrueを返す
ProcessChunkWorkload = True
End Function
—
3. シニアエンジニアが押さえるべき「極限の知見」と罠
上記のコードは美しく見えるかもしれない。しかし、Accessという特異な環境でこれをプロダクション投入するには、さらに深いシステムレベルの洞察が必要だ。
1. メモリリークとCOMコンポーネントの残置
VBAのガベージコレクションは参照カウント方式(Reference Counting)に依存している。特に `CurrentDb` は呼び出すたびに新しいDAO.Databaseオブジェクトをヒープ上に生成する。
タイマーイベントのように「数秒おきに何千回も実行される処理」の中で `CurrentDb.OpenRecordset` を安易に連発し、かつローカル変数への格納と明示的な `Set x = Nothing` を怠ると、数時間でAccessのメモリ使用量がギガ単位に膨れ上がり、最悪の場合Jet/ACEエンジンがクラッシュする。
上記コードの通り、スコープを限定し、抜け際に必ず `Set db = Nothing` を行うことが絶対の鉄則である。
2. 例外処理(Error Handler)の迷子とフラグの死
非同期処理における最大の恐怖は、エラー発生時に `m_IsProcessing = True` のままエラーハンドラーにトラップされ、フラグが永遠に戻らなくなる現象だ。これによりバックグラウンド処理は「永久に停止した状態」に陥る。
`On Error GoTo` を記述する際は、必ず出口(`ErrorHandler_Exit`)でフラグを強制的に `False` にリセットするガード節を設置しなければならない。
3. スレッドセーフティの幻想
VBAはシングルスレッドであるため、C#やJavaのような「データ競合(Data Race)」は原理的に起きない。しかし、「UIスレッドとタイマーイベントによるロジックの錯綜」は起きる。
例えば、ユーザーがタイマー処理の最中にフォームを閉じようとした場合(`Form_Unload`)、レコードセットやデータベースオブジェクトが宙ぶらりんになる。
これを防ぐため、フォームクローズ時には必ず `Me.TimerInterval = 0` を明示的に設定し、タイマーの割り込みを完全に停止させてから破棄プロセスへ移行する配慮が必要だ。
—
4. さらに高みを目指す者へ:Windows APIとの融合
もし、Accessのタイマー(最小解像度はおおむね55ms程度、精度にムラがある)では許されない厳密なミリ秒単位のポーリングや、ウィンドウハンドルを持たないバックグラウンド処理が必要な場合は、`SetTimer` というWindows API(User32.dll)を直接叩く手法に移行する。
しかし、そこまで複雑なアーキテクチャが必要になる頃には、Accessというプラットフォームの限界を超えている。その場合は素直に .NET Framework / .NET Core によるWindowsサービスやコンソールアプリへ移行すべきだ。
「Accessでどこまで粘り、どこで見切りをつけるか」
それを見極めることこそが、真のシニアアーキテクトの仕事である。
`TimerInterval` は、レガシーなAccessアプリケーションに「命の鼓動」を与えるための洗練されたメスだ。正しく扱い、メモリを慈しみ、例外を制圧したとき、あなたの作るAccessシステムは、もはや単なる「エクセル毛嫌いおじさんのデータベース」ではなく、堅牢なエンタープライズ・ノードへと昇華する。
