VBScriptを掌握する極限の知見:`WScript.Sleep`の呪縛を断つ非同期風イベントループの構築
レガシーシステムの深部、あるいはFA(ファクトリーオートメーション)やキオスク端末の裏側で、今なお静かに稼働し続けるVBScript。コンパイル不要の手軽さゆえに重宝される一方で、そのシングルスレッドかつプリミティブなランタイム構造は、多くのエンジニアを絶望させてきた。
特にWSH(Windows Script Host)環境における最大の足枷、それが `WScript.Sleep` である。
このメソッドを実行した瞬間、スクリプトの実行スレッドは完全に凍結(ブロック)される。GUIからの入力も、外部からのイベント通知も、一切の処理が停止する。もし君が「30秒おきにログファイルを監視しつつ、5分おきにAPIへハートビートを送信したい」と考えたとき、`WScript.Sleep 30000` を素朴に配置すれば、後者のタスクは前者のブロックに巻き込まれ、正確なスケジューリングは完全に崩壊する。
今回は、VBScriptの限界領域を突破し、`Timer`関数を用いた高精度なタイムスケジュール制御――すなわち、「ノンブロッキング・イベントループ」の極限実装を解説する。
—
1. なぜ `WScript.Sleep` は悪なのか?
`WScript.Sleep` はOSのCPUリソースを解放する利点はあるが、「スクリプトの全停止」という致命的な副作用を持つ。
‘ 【アンチパターン】これではマルチタスクは実現できない
Do
Call HeavyLogProcess()
WScript.Sleep 30000 ‘ ← ここで30秒間、世界が停止する
Call SendHeartbeat()
WScript.Sleep 300000 ‘ ← さらに5分停止
Loop
このアプローチでは、タスクの実行間隔が「前後の処理時間の累積」に依存するため、厳密な定常間隔(Interval)を維持できない。真に求められるのは、時間を止めるのではなく、「現在時刻を観測し続け、条件を満たしたタスクだけを瞬発的に発火させる」という設計思想である。
—
2. アーキテクチャ設計:VBScriptにおけるイベントループ
VBScriptには、Node.jsのようなネイティブな非同期イベントループや、C#のような `async/await` は存在しない。しかし、`Timer` 関数(真夜中からの経過秒数を単精度浮動小数点数で返す)を利用すれば、ミリ秒単位の経過時間を自前でトラッキングできる。
ここに、複数のタスクオブジェクト(または独立した関数)を常時監視するメイン・イベントループを構築する。
核心となる制御理論
1. タイムスタンプの保持: 各タスクごとに「前回実行された時刻」を保持する。
2. デルタ時間の計算: ループの各周回で `Timer` を呼び出し、現在時刻と前回実行時刻の差分(経過時間)を計算する。
3. 閾値判定: 経過時間が指定されたインターバルを超えていればタスクを実行し、前回実行時刻を現在時刻に更新する。
4. CPU枯渇の防止: ループがフルスロットルでCPUコアを100%消費するのを防ぐため、極めて短いスリープ(例: `WScript.Sleep 10`)を挟む。この「10ミリ秒」は全体のスケジュール精度を損なわず、OSのコンテキストスイッチを正常に機能させるための絶妙なバッファである。
—
3. 実装コード:マルチタスク・イベントループエンジン
以下のコードは、異なる周期で動く3つのタスク(高頻度ポーリング、中頻度ファイル監視、低頻度死活監視)を、単一のスレッド上で完全に調停・並行実行する実用サンプルである。
Option Explicit
‘ ==============================================================================
致命的なブロックを回避する非同期風イベントループ・エンジン
‘ ==============================================================================
Sub Main()
WScript.Echo “=== Event Loop Engine Started (Press Ctrl+C to stop) ===”
‘ 各タスクの前回実行タイムスタンプ(Timer関数の戻り値を格納)
Dim nextTaskA, nextTaskB, nextTaskC
‘ インターバル設定(単位:秒)
Const INTERVAL_A = 2.0 ‘ タスクA: 2秒おき(例: 画面描画・状態確認)
Const INTERVAL_B = 7.0 ‘ タスクB: 7秒おき(例: キュー監視)
Const INTERVAL_C = 15.0 ‘ タスクC: 15秒おき(例: ログフラッシュ)
Dim currentTime
‘ 初期化:即座に初回実行されるようオフセットを与える
currentTime = Timer
nextTaskA = currentTime
nextTaskB = currentTime
nextTaskC = currentTime
‘ メイン・イベントループ
Do
currentTime = Timer
‘ 日付変更線(深夜0時)を跨いだ場合のTimerリセット対策
‘ (Timerは0:00:00からの秒数を返すため、深夜0時を跨ぐと値が0に戻る)
If currentTime < nextTaskA Then nextTaskA = currentTime
If currentTime < nextTaskB Then nextTaskB = currentTime
If currentTime < nextTaskC Then nextTaskC = currentTime
' --- タスク A の評価と実行 ---
If (currentTime - nextTaskA) >= INTERVAL_A Then
Call Task_HighFrequencyPoll()
nextTaskA = currentTime ‘ 実行時刻を更新
End If
‘ — タスク B の評価と実行 —
If (currentTime – nextTaskB) >= INTERVAL_B Then
Call Task_MediumFileMonitor()
nextTaskB = currentTime
End If
‘ — タスク C の評価と実行 —
If (currentTime – nextTaskC) >= INTERVAL_C Then
Call Task_LowHeartbeat()
nextTaskC = currentTime
End If
‘ 【極意】CPU使用率の暴騰を防ぐためのマイクロ・スリープ
‘ 全体停止ではなく、次のイベント評価までの極短のインターバル
WScript.Sleep 10
Loop
End Sub
‘ ==============================================================================
‘ 個別タスク定義
‘ ==============================================================================
Sub Task_HighFrequencyPoll()
WScript.Echo “[” & FormatDateTime(Now, 4) & “.” & Right(CStr(Timer), 3) & “] [Task A] High-frequency poll executed.”
End Sub
Sub Task_MediumFileMonitor()
WScript.Echo “[” & FormatDateTime(Now, 4) & “] [–> Task B] Medium file check executed.”
‘ 【実務的知見】メモリ最適化とオブジェクト解放の徹底
‘ ファイルシステム操作などCOMオブジェクトを使う場合は必ずローカルスコープで生成・破棄する
Dim fso, targetFile
Set fso = CreateObject(“Scripting.FileSystemObject”)
‘ 擬似的な処理
If fso.FolderExists(“C:\Windows\Temp”) Then
‘ 処理…
End If
‘ 明示的なオブジェクト破棄(VBScriptのガベージコレクタへの依存を断つ)
Set fso = Nothing
End Sub
Sub Task_LowHeartbeat()
WScript.Echo “[” & FormatDateTime(Now, 4) & “] [====> Task C] Low-frequency heartbeat sent.”
End Sub
‘ エンジン起動
Call Main()
—
4. チーフアーキテクトが指摘する「実運用上の罠と最適化」
このアーキテクチャを現場のプロダクション環境に投入する際、以下の「VBScript特有の落とし穴」に直面する。これらを事前に潰すことが、真のプロフェッショナルである。
① `Timer` 関数の精度と「日付け変更線」の呪い
VBScriptの `Timer` 関数は、深夜0時0分0秒からの経過秒数を単精度浮動小数点(Single)で返す。そのため、深夜0時を跨いだ瞬間に `Timer` の値が `0` にリセットされる。
上記のコードでは `If currentTime < nextTaskA Then nextTaskA = currentTime` という防衛的コードを挟むことで、日付変更時のハングアップを防いでいるが、ミッションクリティカルなシステムではこの挙動を前提とした設計が不可欠である。
② COMオブジェクトのメモリリークとライフサイクル管理
イベントループが長期間(数週間〜数ヶ月)稼働する場合、タスク内で生成したCOMオブジェクト(`Scripting.FileSystemObject`, `ADODB.Connection`, `WScript.Network` など)の解放漏れは、致命的なメモリリーク(VBScriptランタイム自体のメモリ肥大化)を引き起こす。
- 鉄則: タスク内で生成したオブジェクトは、そのタスクのスコープが終了する前に必ず `Set obj = Nothing` で明示的に解放すること。
- さらに高度な知見: データベース接続などをタスク内で毎回 `Open/Close` するのではなく、メインループの初期化時にグローバル(またはStatic)なコネクションを保持し、コネクションプリングの恩恵を受ける設計も検討せよ。ただし、ネットワーク切断耐性を考慮するなら、タスクごとに接続を確立・破棄する方がレガシー環境では安全な場合が多い。
③ エラーハンドリング(`On Error Resume Next` の毒と薬)
イベントループの途中で予期せぬエラー(ネットワーク切断、ファイルロック等)が発生した際、何の対策もしていなければスクリプト全体が異常終了し、すべてのタスクが停止する。
これを防ぐため、各タスクの内部でのみ局所的なエラーハンドリングを行うこと。メインループ自体を `On Error Resume Next` で汚染してはならない。
Sub Task_MediumFileMonitor()
On Error Resume Next ‘ このタスク内でのみエラーをトラップ
‘ 危険な外部リソースアクセス
Dim fso: Set fso = CreateObject(“Scripting.FileSystemObject”)
‘ … 処理 …
If Err.Number <> 0 Then
‘ ログに記録して処理を継続する
WScript.Echo “[ERROR in Task B] ” & Err.Description
Err.Clear
End If
Set fso = Nothing
On Error Goto 0 ‘ エラー監視を復元
End Sub
—
総括
VBScriptはレガシーな言語であるかもしれない。しかし、そのランタイムの仕様と制約を骨の髄まで理解していれば、`WScript.Sleep` のような原始的な罠を回避し、モダンな非同期処理のエッセンスを抽出することは十分に可能である。
「言語が古いからできない」のではない。「構造を理解していないから書けない」だけだ。
このイベントループ・パターンを武器に、君の管理する自動化スクリプトを、止まることのない堅牢なシステムへと昇華させてほしい。
