Access VBAを掌握する極限の知見:TimerIntervalとOnTimerによる擬似マルチスレッドの極意
Accessは、その手軽さと強力なデータ管理能力の裏腹に、「完全なシングルスレッド・アプリケーション」という致命的な制約を抱えている。重い処理、例えば数万件のレコードを処理するループや、外部Web APIへの同期通信、巨大なExcelレポートの生成などを実行した瞬間、AccessのUIは完全に凍結し、Windowsからは「応答なし」の烙印を押される。ユーザーはイライラを募らせ、時には強制終了によってデータベースを破損させる。
「Accessだから仕方ない」と諦めるのは、アーキテクトとして三流の言い訳に過ぎない。
VBAには真のマルチスレッド(別スレッドでのコード実行)機能は存在しないが、Windowsメッセージループの仕組みとAccessの`Form.TimerInterval`および`Form.OnTimer`イベントを完全に理解し、制御下置くことで、バックグラウンドでの「擬似非同期処理(マルチスレッド風処理)」を実装することが可能だ。
本稿では、レガシーなAccess環境において、UIの応答性を維持しながら裏で重いタスクを巧みにさばく、極限のアーキテクチャを解説する。
—
1. Accessシングルスレッド制約の正体とタイマーイベントのメカニズム
AccessのVBAランタイムは、メインのUIスレッドと同一のコンテキストで動作する。したがって、VBAで無限ループや重い処理を実行すると、メッセージキューの処理がブロックされ、画面の再描画やユーザーの入力操作が一切受け付けられなくなる。
ここで登場するのが `TimerInterval` プロパティだ。
このプロパティにミリ秒単位の時間(例: `1000` = 1秒)を設定すると、指定したインターバルごとに、Windowsのメッセージキューにタイマー割り込みイベントが挿入される。
重要なのは、「タイマーイベント(`OnTimer`)はメインのメッセージループの隙間に割り込んで実行される」という点だ。つまり、重い一括処理を一度に行うのではなく、処理を「小さなチャンク(断片)」に分割し、タイマーイベントの1回の発火につき1チャンクずつ処理を進めれば、間にUIの描画やユーザー操作を挟み込むことができ、結果として「裏で何かが動いている(非同期処理)」状態を作り出せるのである。
—
2. 実装アーキテクチャ:ステートマシンによる非同期タスクランナー
擬似マルチスレッドを実現するためには、グローバル変数やフォームのモジュールレベル変数を用いて、現在の処理の「状態(State)」を管理するステートマシン(状態機械)を構築する必要がある。
以下に、大量データのエクスポートや外部API連携を想定した、実用的な「非同期タスクランナーフォーム」のコードを示す。
【実装コード】非同期処理制御用フォーム(`Form_frmAsyncWorker`)
Option Explicit
‘ — 定数定義 —
Private Const TIMER_INTERVAL_ACTIVE As Long = 100 ‘ 処理中のタイマー間隔 (0.1秒)
Private Const TIMER_INTERVAL_IDLE As Long = 0 ‘ 停止中のタイマー間隔
‘ — 状態管理用列挙体 —
Private Enum TaskState
StateIdle = 0
StateRunning
StatePaused
StateError
StateCompleted
End Enum
‘ — モジュールレベル変数(ライフサイクル管理) —
Private m_CurrentState As TaskState
Private m_TargetRecordCount As Long
Private m_CurrentIndex As Long
Private m_ProcessingData() As String ‘ 処理対象のデータ配列(例)
‘ フォームロード時の初期化
Private Sub Form_Load()
On Error GoTo ErrorHandler
m_CurrentState = StateIdle
Me.TimerInterval = TIMER_INTERVAL_IDLE
‘ UIの初期化
Me.ProgressBar.Width = 0
Me.txtStatus.Value = “待機中…”
Exit Sub
ErrorHandler:
Call LogError(“Form_Load”, Err.Number, Err.Description)
End Sub
‘ 【非同期処理の開始トリガー】外部やボタンから呼び出される
Public Sub StartAsyncProcess(ByRef dataArray() As String)
If m_CurrentState = StateRunning Then
MsgBox “既にバックグラウンドタスクが実行中です。”, vbExclamation
Exit Sub
End If
‘ データの受け渡しと状態の初期化
m_ProcessingData = dataArray
m_TargetRecordCount = UBound(dataArray) – LBound(dataArray) + 1
m_CurrentIndex = LBound(dataArray)
m_CurrentState = StateRunning
Me.txtStatus.Value = “バックグラウンド処理実行中…”
‘ タイマー起動(ここで非同期ループが回り始める)
Me.TimerInterval = TIMER_INTERVAL_ACTIVE
End Sub
‘ 【心臓部】タイマーイベント(非同期実行ループ)
Private Sub Form_Timer()
‘ 厳密な再入防止ガード(タイマー処理中の多重発火を防ぐ)
Static isProcessing As Boolean
If isProcessing Then Exit Sub
isProcessing = True
On Error GoTo TimerError
If m_CurrentState = StateRunning Then
‘ —————————————————-
‘ 1回のタイマー発火で行う「1チャンク」の処理
‘ (例として、1回につき1件ずつ処理を進める)
‘ —————————————————-
If m_CurrentIndex <= UBound(m_ProcessingData) Then
' --- ここに重い実処理を記述(例:API送信や重いDB更新) ---
Call ExecuteHeavyTaskChunk(m_ProcessingData(m_CurrentIndex))
' 進捗率の更新
m_CurrentIndex = m_CurrentIndex + 1
Call UpdateUIProgress
Else
' すべての処理が完了
m_CurrentState = StateCompleted
Me.TimerInterval = TIMER_INTERVAL_IDLE
Me.txtStatus.Value = "処理が正常に完了しました。"
MsgBox "バックグラウンドタスクが完了しました。", vbInformation
End If
End If
isProcessing = False
Exit Sub
TimerError:
isProcessing = False
m_CurrentState = StateError
Me.TimerInterval = TIMER_INTERVAL_IDLE
Me.txtStatus.Value = "エラー発生により中断しました。"
Call LogError("Form_Timer", Err.Number, Err.Description)
MsgBox "エラーが発生しました: " & Err.Description, vbCritical
End Sub
' 1チャンクの処理をシミュレート
Private Sub ExecuteHeavyTaskChunk(ByRef target As String)
' 実際にはここでCurrentDb.ExecuteやHTTPリクエスト等を行う
' DoEventsをあえて入れなくても、タイマーイベント自体がUIスレッドを解放する
Debug.Print "Processing: " & target
End Sub
' UIの進捗表示更新
Private Sub UpdateUIProgress()
Dim progressRate As Double
progressRate = CDbl(m_CurrentIndex) / CDbl(m_TargetRecordCount)
' プログレスバー風のラベル幅変更(例)
Me.lblProgressIndicator.Width = 3000 progressRate
Me.txtDetail.Value = m_CurrentIndex & " / " & m_TargetRecordCount & " 処理完了"
End Sub
Private Sub LogError(ByVal procName As String, ByVal errNum As Long, ByVal errDesc As String)
' エラーログ出力処理
Debug.Print "[" & Now & "] " & procName & " Error " & errNum & ": " & errDesc
End Sub
---
3. シニアエンジニアが押さえるべき「メモリ最適化」と「罠」
この手法は非常に強力だが、Access特有のメモリ管理の癖やVBAの仕様を理解していないと、メモリリークや予期せぬクラッシュを引き起こす。極限の環境で安定稼働させるための知見を以下に記す。
① 再入防止(Reentrancy Guard)の徹底
タイマー間隔を短く(例: 50ms〜100ms)設定した場合、前回の `OnTimer` 内の処理が終わらないうちに次のタイマーイベントが発火する危険性がある。
コード例でも示した通り、`Static isProcessing As Boolean` などのフラグを用いて、処理中の多重突入を確実にブロックしなければ、オブジェクトの競合やメモリ破損を引き起こす。
② オブジェクトの明示的解放とスコープ管理
バックグラウンド処理内で `DAO.Recordset` や `ADODB.Recordset`、あるいは外部COMオブジェクト(ExcelやFileSystemObjectなど)を生成・破棄する場合、ガベージコレクションに頼ってはならない。
ループの1チャンクごとにオブジェクトを生成・破棄する場合は、必ずループ内またはプロシージャの退出時に `Set xxx = Nothing` を明示し、スコープを最小限に絞ること。
‘ 良い例:チャンクごとに確実に解放
Private Sub ExecuteHeavyTaskChunk(ByVal id As String)
Dim db As DAO.Database
Dim rs As DAO.Recordset
Set db = CurrentDb
Set rs = db.OpenRecordset(“SELECT FROM T_Master WHERE ID = ‘” & id & “‘”, dbOpenSnapshot)
If Not rs.EOF Then
‘ 処理…
End If
‘ 明示的な解放
rs.Close
Set rs = Nothing
Set db = Nothing ‘ CurrentDbの解放もローカル変数であれば原則推奨
End Sub
③ `CurrentDb` の乱用に注意
Access VBAで頻繁に使われる `CurrentDb` は、呼び出すたびに新しいデータベースオブジェクトのインスタンスをメモリ上に生成する。バックグラウンド処理のタイマー内で毎回 `CurrentDb` を安易に呼び出すと、急速なメモリ消費(リソースリーク)を引き起こす原因となる。
長期にわたるバックグラウンド処理では、処理開始時に `CurrentDb` をモジュールレベル変数に保持し、それを使い回すか、適切なタイミングで参照を破棄する設計が不可欠である。
—
4. システム間連携・レガシー保守への応用
この擬似マルチスレッド技術は、単なるプログレスバーの描画にとどまらず、「ノンブロッキングな外部システム連携」において真価を発揮する。
例えば、社内基幹システムのAccessから、REST APIを通じてクラウド上のサーバーへ大量のデータを非同期で順次POSTしていくようなケースを想像してほしい。同期通信でこれをやると、ネットワークの遅延(タイムアウト待ちなど)が発生した瞬間にAccess全体が固まり、ユーザーは業務を継続できなくなる。
しかし、タイマーイベントによるチャンク送信(1回に1レコード、あるいは100件ずつ送信)を採用していれば、仮に一時的なネットワーク切断やAPI側の応答遅延が発生しても、AccessのUIは軽快に動き続け、ユーザーは別の作業(データの参照など)を行うことができる。エラーが発生した場合も、ステートマシンが `StateError` をキャッチして安全に処理を中断・ログ記録できるため、システムの堅牢性が劇的に向上する。
—
総括
Accessはレガシーなプラットフォームと揶揄されがちだが、その内部構造とイベント駆動のメカニズムを突き詰めれば、モダンなアプリケーションに匹敵する高度な非同期処理を実装することが可能だ。
「シングルスレッドだから重い処理は無理だ」と諦めるのではなく、時間を微小な断片に分割し、Windowsのメッセージループと協調動作させる。このアーキテクチャの理解こそが、凡百のVBAプログラマーと、システムを極限まで最適化するシニアエンジニアを分かつ境界線である。
