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

スポンサーリンク

Access VBAを掌握する極限の知見:TimerIntervalとOnTimerによる擬似非同期マルチスレッドの実装

レガシーシステムの象徴として語られがちなMicrosoft Accessだが、その実態は、適切に調教すれば数万人の基幹業務を支え続ける極めて高効率なローカルDB駆動エンジンである。

しかし、Access(VBAエンジン)の最大にして唯一の致命的弱点は「シングルスレッド制約」にある。重いバッチ処理、外部APIとの同期通信、数万件のレコードセット走査をメインスレッドで直列実行した瞬間、UIは完全にフリーズし、Windowsは「応答なし」の烙印を押す。エンドユーザーは不安に駆られ、タスクマネージャーから容赦なくプロセスを強制終了するだろう。

「Accessだからフリーズは仕方ない」などと言い訳をするエンジニアは、ここで淘汰されるべきだ。
我々シニアアーキテクトに求められるのは、制約の中で限界を突破する実装力である。今回は、フォームの `TimerInterval` と `OnTimer` イベントを極限までハックし、非同期処理(擬似マルチスレッド)を美しく実装するアーキテクチャを解説する。

1. なぜ「タイマー駆動型ステートマシン」なのか?

マルチスレッドを疑似的に実現するアプローチとして、標準モジュールでの `Sleep` 関数の呼び出しや無限ループを考える者がいるが、それは愚の骨頂だ。`Sleep` はメインスレッドのメッセージキューを完全にブロックするため、UIの描画すら停止する。

我々が取るべきアプローチは、「時間を微小なチャンク(単位)に分割し、OSのイベントループに制御を返す」ことだ。
具体的には以下のメカニズムで駆動する。

1. 非同期タスクを「ステート(状態)」に分解する(例:初期化 → 抽出 → 加工 → 書き込み → 終了)。
2. `TimerInterval`(ミリ秒)を例えば `10`(0.01秒)に設定し、`OnTimer` で1ステップずつ処理を進める。
3. 1ステップが終わったら即座に制御をOSに返し、UIスレッドの呼吸(描画やユーザー操作の受付)を維持する。

このアーキテクチャを実装するためには、グローバル変数の汚染を防ぎつつ、処理の進捗状態を保持する「ステートマシン構造」が不可欠となる。

2. 実装コード:ノンブロッキング・データバッチプロセッサ

以下に、実務でそのまま使える堅牢なクラスおよびフォームモジュールの実装を示す。
今回は、重いCSVインポート処理や大量データ更新を、UIをフリーズさせずにバックグラウンド実行するモデルだ。

フォームモジュール(例:`frmAsyncProcessor`)

Option Compare Database
Option Explicit

‘ =========================================================================
‘ 圧倒的なアーキテクチャのための定数定義
‘ =========================================================================
Private Const TIMER_INTERVAL_ACTIVE As Long = 50 ‘ 処理中のタイマー間隔(ミリ秒)
Private Const TIMER_INTERVAL_IDLE As Long = 0 ‘ 停止中のタイマー間隔

‘ =========================================================================
‘ 状態管理(ステート)用変数
‘ =========================================================================
Private m_ExecutionState As Long
Private m_CurrentRecord As Long
Private m_TotalRecords As Long
Private m_Rs As DAO.Recordset

‘ ステート定義
Private Enum ProcessState
State_Idle = 0
State_Initializing = 1
State_Processing = 2
State_Finalizing = 3
State_Error = 99
End Enum

‘ =========================================================================
‘ イベントプロシージャ:処理開始
‘ =========================================================================
Private Sub cmdStartAsync_Click()
On Error GoTo ErrorHandler

‘ 多重起動の防止
If m_ExecutionState <> State_Idle Then
MsgBox “既にバックグラウンド処理が実行中です。”, vbExclamation
Exit Sub
T
End If

‘ UIのロック(操作制限はかけるが、フリーズさせない)
Me.cmdStartAsync.Enabled = False
Me.ProgressBar.Value = 0
Me.lblStatus.Caption = “初期化中…”

‘ 状態を初期化フェーズへ移行
m_ExecutionState = State_Initializing

‘ タイマー起動(ここでイベントループに処理を委譲)
Me.TimerInterval = TIMER_INTERVAL_ACTIVE
Exit Sub

ErrorHandler:
Call HandleError(“cmdStartAsync_Click”)
End Sub

‘ =========================================================================
‘ 極限の核心:OnTimer イベントによる擬似マルチスレッド処理
‘ =========================================================================
Private Sub Form_Timer()
‘ タイマーイベント内での多重発火を防ぐため、即座にタイマーを一時停止(または排他制御)
Me.TimerInterval = TIMER_INTERVAL_IDLE

On Error GoTo TimerErrorHandler

Select Case m_ExecutionState

Case State_Initializing
‘ 1. 初期化処理(重いクエリのオープンなど)
Dim db As DAO.Database
Set db = CurrentDb

‘ 例として重いローカルテーブルを対象とする
Set m_Rs = db.OpenRecordset(“SELECT FROM T_HeavyDataBuffer WHERE Processed = 0”, dbOpenDynaset, dbSeeChanges)

If m_Rs.EOF Then
m_TotalRecords = 0
m_ExecutionState = State_Finalizing
Else
m_Rs.MoveLast
m_TotalRecords = m_Rs.RecordCount
m_Rs.MoveFirst
m_CurrentRecord = 0

Me.ProgressBar.Max = m_TotalRecords
m_ExecutionState = State_Processing
End If

Case State_Processing
‘ 2. メイン処理(全体のフリーズを防ぐため、1回につき「10件だけ」処理して抜ける)
Dim chunkCount As Long
chunkCount = 0

Do While Not m_Rs.EOF And chunkCount < 10 ' --- ここに重いビジネスロジックを記述 --- m_Rs.Edit m_Rs("Processed") = True m_Rs("ProcessedDate") = Now m_Rs.Update m_CurrentRecord = m_CurrentRecord + 1 chunkCount = chunkCount + 1 m_Rs.MoveNext Loop ' UIの更新(フリーズしていない証拠) Me.ProgressBar.Value = m_CurrentRecord Me.lblStatus.Caption = "処理中... (" & m_CurrentRecord & " / " & m_TotalRecords & " 件)" ' 終了判定 If m_Rs.EOF Then m_ExecutionState = State_Finalizing End If Case State_Finalizing ' 3. 終了処理(リソースの解放) If Not m_Rs Is Nothing Then m_Rs.Close Set m_Rs = Nothing End If Me.lblStatus.Caption = "処理が正常に完了しました。" Me.cmdStartAsync.Enabled = True m_ExecutionState = State_Idle Exit Sub ' タイマーを再開しないことで完全停止 Case State_Error GoTo TimerErrorHandler End Select ' 処理が継続中の場合のみ、タイマーを再有効化して次のスライスへ If m_ExecutionState <> State_Idle Then
Me.TimerInterval = TIMER_INTERVAL_ACTIVE
End If
Exit Sub

TimerErrorHandler:
m_ExecutionState = State_Error
Call HandleError(“Form_Timer”)

‘ 異常終了時のクリーンアップ
If Not m_Rs Is Nothing Then
m_Rs.Close
Set m_Rs = Nothing
End If
Me.cmdStartAsync.Enabled = True
Me.lblStatus.Caption = “エラーが発生して中断しました。”
End Sub

Private Sub HandleError(ByVal source As String)
MsgBox “エラーが発生しました [” & source & “]: ” & Err.Description, vbCritical
Me.TimerInterval = TIMER_INTERVAL_IDLE
m_ExecutionState = State_Idle
Me.cmdStartAsync.Enabled = True
End Sub

3. シニアアーキテクトが解説するメモリ最適化と落とし穴

このコードは一見シンプルだが、Access VBAのメモリ管理、ガベージコレクションの挙動を熟知していないと、メモリリークや予期せぬクラッシュを引き起こす。以下の極限の知見を心に刻んでほしい。

① DAO/ADOオブジェクトの「明示的かつ即時」な破棄

VBAのランタイムは参照カウント方式でメモリを管理しているが、特に `Recordset` や `Database` オブジェクトをループ内で暗黙的に生成・放置すると、COMコンポーネントのメモリリークを引き起こす。
終了フェーズ(`State_Finalizing`)およびエラーハンドラの両方で、必ず `If Not m_Rs Is Nothing Then m_Rs.Close: Set m_Rs = Nothing` を実行し、OSへメモリを即座に返還すること。

② チャンクサイズのチューニング

`Form_Timer()` 内で1回あたりに処理するレコード数(上記の例では `chunkCount < 10`)は、システムの負荷とUIの滑らかさのトレードオフだ。

  • 小さすぎる場合: オーバーヘッドが増え、全体の処理時間が延びる。
  • 大きすぎる場合: その間UIの描画がブロックされ、フリーズ感が出る。

対象テーブルのレコードあたりの処理コストを計測し、1回のタイマーサイクルが「5ミリ秒以内」に収まるようチャンクサイズを動的に調整するのがプロの技量だ。

③ バックグラウンド処理中の「フォーム閉じる問題」

ユーザーが処理の途中にフォームの「×」ボタンを押して閉じた場合、メモリ上に存在しないフォームのコントロール(`Me.ProgressBar` など)にアクセスしようとして、「実行時エラー 2448: オブジェクトに値を代入することはできません」 や最悪の場合は強制終了を引き起こす。
これを防ぐため、`Form_Unload` イベントで必ずタイマーを無効化し、実行中フラグが立っている場合は確認メッセージを出すか、処理を安全にアボート(中断)する仕組みを実装しなければならない。

Private Sub Form_Unload(Cancel As Integer)
If m_ExecutionState <> State_Idle Then
If MsgBox(“バックグラウンド処理が実行中です。中断して終了しますか?”, vbYesNo + vbQuestion) = vbYes Then
Me.TimerInterval = TIMER_INTERVAL_IDLE
If Not m_Rs Is Nothing Then
m_Rs.Close
Set m_Rs = Nothing
End If
Else
Cancel = True ‘ 終了をキャンセル
End If
End If
End Sub

4. システム間連携・レガシー保守への応用

この「タイマー駆動型ステートマシン」の真価は、何もローカルのレコードセット処理だけに留まらない。

  • Web API(HTTP通信)との非同期連携:

MSXML2.ServerXMLHTTP を非同期モード (`Async = True`) で呼び出し、完了をタイマーでポーリング監視する。これにより、外部APIの応答待ちでAccess全体がフリーズする悪夢から解放される。

  • 巨大なテキスト/CSVファイルのストリーミング読み込み:

ファイルを一定バイト数ずつ読み込み、データベースへバルクインサートしていくバッチ処理を、UIを完全に操作可能な状態で裏で回し続けることが可能になる。

総括

Accessは「おもちゃのDB」などではない。その内部アーキテクチャの制約を正確に理解し、Windowsメッセージループとタイマーイベントの本質を見極めた者にとっては、極めて高い開発生産性と堅牢性を誇る強力なプラットフォームへと変貌する。

「フリーズするAccess」に別れを告げよ。お前が手元のコードを書き換えたその瞬間から、そのレガシーシステムは真のエンタープライズ仕様へと進化するのだ。

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