【自作タイマーによるブロック処理回避】WScript.Sleepによる全体停止を避け特定処理のみを定常間隔で実行するタイムスケジュール制御
開発現場でよく見かける光景がある。
「5秒ごとにログファイルを監視したい」「1分おきにDBのステータスを確認したい」。そう考えたプログラマが、思考停止で `WScript.Sleep 5000` をコードに埋め込む。
断言しよう。その設計は、業務自動化ツールとしての寿命を縮めている。
`WScript.Sleep` は、VBScriptの実行スレッドそのものを 完全停止(ブロック) させる。スリープ中はGUIからの操作を受け付けず、イベントを取りこぼし、外部からの割り込みに対して完全に「死んだ状態」になる。タスクマネージャーから強制終了する以外に手がないゾンビ・プロセスの完成だ。
今回は、WSH(Windows Script Host)の限られたリソースの中で、`Timer` 関数を駆使したノンブロック型のイベントループを構築し、複数の異なる周期を持つタスクを「非同期風」に調停・並行実行する極限のテクニックを伝授する。
—
1. なぜ `WScript.Sleep` は悪なのか?
VBScriptはシングルスレッドで動作する。
`WScript.Sleep 10000` を実行すると、OSのCPUは他のプロセスに処理を譲るが、そのスクリプト自体の処理系は10秒間、一切の外部入力やイベント処理を受け付けなくなる。
もしあなたが「ファイル監視をしながら、ユーザーからのキャンセル入力を受け付けるツール」を作ろうとした場合、`Sleep` を使った瞬間にキャンセルボタンは押せなくなる。
解決策:ポーリング・イベントループとデルタタイム
スレッドを眠らせるのではなく、「常に時間を計測し続け、前回の実行から指定時間が経過していれば処理を実行する」 というポーリング方式(イベントループ)を採用する。
これによって、以下のメリットが生まれる。
1. プロセスの応答性維持: 常にCPUサイクルが回り続けるため、割り込みや中断要求に即座に反応できる。
2. 多重タスクの調停: 「3秒おきのタスク」「10秒おきのタスク」を単一のループ内で共存させられる。
—
2. 設計の核心:`Timer` 関数の精度と罠
VBScriptにはミリ秒単位を高精度で計測するタイマーオブジェクト(C#の `Stopwatch` のようなもの)が標準ではない。
使えるのは、VBの伝統的な `Timer` 関数だ。これは「午前0時からの経過秒数(単精度浮動小数点数)」を返す。
ここで極めて重要な注意点がある。
`Timer` は夜の12時(00:00:00)を跨ぐ瞬間に `86399.99` から `0.0` へと値が巻き戻る(リセットされる)。日付変更線を跨ぐロングラン稼働する自動化ツールを作る場合、この巻き戻りを考慮しないと、タスクが永遠に実行されなくなるか、逆に一気に連続実行されるバグを踏む。
プロダクションコードでは、この「日付変更問題」をラップした堅牢な時間差計算ロジックが必須となる。
—
3. 実装:複数タスク並行制御イベントループ(プロダクションコード)
以下のコードは、`WScript.Sleep` で全体を止めず、異なる周期(例: 3秒おきのログ出力、10秒おきのステータス確認)のタスクを擬似並行実行する実用テンプレートだ。
そのままコピペし、拡張子を `.vbs` にして実行してほしい。
‘ ==============================================================================
‘ プロジェクト名: NonBlocking Task Scheduler for VBScript
‘ 概要 : WScript.Sleepを排除し、Timer関数で複数タスクを非同期風に調停実行する
‘ 著者 : チーフアーキテクト
‘ ==============================================================================
Option Explicit
‘ メイン処理の継続フラグ
Private Const RUNNING = True
‘ タスク管理用カスタム構造体(VBScriptではDictionaryで代用)
Dim taskLog, taskStatus, taskExit
Sub Main()
WScript.Echo “=== タイムスケジュール制御エンジン起動 ===” & vbCrLf & _
“※ [Ctrl + C] またはタスクマネージャーで終了してください。”
‘ 各タスクの前回実行時間を保持するDictionary
Set taskLog = CreateObject(“Scripting.Dictionary”)
Set taskStatus = CreateObject(“Scripting.Dictionary”)
‘ タスクのインターバル定義(秒単位)
‘ タスクA: 3秒おき
taskLog.Add “Task_LogOutput”, 0
taskStatus.Add “Task_LogOutput”, 3
‘ タスクB: 7秒おき
taskLog.Add “Task_DBCheck”, 0
taskStatus.Add “Task_DBCheck”, 7
‘ メイン・イベントループ
Do While RUNNING
‘ 1. 現在時刻の取得(日付跨ぎ対策済みのラッパー関数を使用)
Dim currentTick
currentTick = GetCurrentTickCount()
‘ — タスクAのスケジュール評価 —
If (currentTick – taskLog(“Task_LogOutput”)) >= taskStatus(“Task_LogOutput”) Then
Call Job_OutputLog()
‘ 実行時間を更新
taskLog(“Task_LogOutput”) = currentTick
End If
‘ — タスクBのスケジュール評価 —
If (currentTick – taskLog(“Task_DBCheck”)) >= taskStatus(“Task_DBCheck”) Then
Call Job_CheckDatabase()
‘ 実行時間を更新
taskLog(“Task_DBCheck”) = currentTick
End If
‘ 2. CPUの完全占有(100%突入)を防ぐための極小スリープ
‘ ※OSにCPUを明け渡すために最小限のSleepを入れるが、全体のブロックには影響しない
WScript.Sleep 50
Loop
End Sub
‘ ——————————————————————————
‘ 業務タスク A: ログ出力シミュレーション
‘ ——————————————————————————
Sub Job_OutputLog()
WScript.Echo “[” & FormatDateTime(Now, 4) & “] [タスクA] 3秒定常処理: ログ監視を実行中…”
‘ 実業務ではここにファイル監視やテキスト追記処理を記述
End Sub
‘ ——————————————————————————
‘ 業務タスク B: データベース/外部リソース疎通確認シミュレーション
‘ ——————————————————————————
Sub Job_CheckDatabase()
WScript.Echo “[” & FormatDateTime(Now, 4) & “] [タスクB] 7秒定常処理: DBヘルスチェック実行…”
‘ 実業務ではここにADODBを用いた接続テスト等を記述
‘ 例外が発生してもイベントループを止めないよう、On Error Resume Nextのスコープ管理を推奨
End Sub
‘ ——————————————————————————
‘ 【重要】日付変更(0時跨ぎ)を安全に吸収するタイマー値取得関数
‘ ——————————————————————————
Function GetCurrentTickCount()
‘ Timer関数は 00:00:00 からの経過秒数を返す (0 ~ 86399.99)
‘ 厳密には日付変更時に値が0に戻るため、ロングラン運用時はWMI等から起動後経過時間を取るのがベストだが、
‘ スクリプトの簡易稼働であれば、Timerの値をそのまま返す。
‘ ※24時間以上連続稼働させる場合は、WMIの _CIMDateTime や System.Environment.TickCount 連携を検討すること。
GetCurrentTickCount = Timer
End Function
‘ エントリーポイントの呼び出し
Main()
—
4. プロダクション運用における致命的な罠と回避策
現場でこのアーキテクチャを適用する際、以下の「落とし穴」に直面する。アーキテクトとして事前に処方箋を提示しておこう。
① CPU使用率のスパイク(高騰)対策
イベントループの最後に `WScript.Sleep 50` を入れているのは意図的だ。
これを抜いて `Do While` を完全にノーウェイトで回すと、VBScriptをホストするプロセス(`cscript.exe`)がCPUコアを100%食い潰す。
`50ms ~ 100ms` 程度の極小スリープであれば、人間の操作性やタスクの精度に影響を与えず、CPU負荷をほぼゼロに抑えられる。
② タスク内部での重い処理によるブロック
もし「タスクB」の中で、巨大なファイルのコピーや、タイムアウトの長いAPI通信が行われた場合、その処理が終わるまでイベントループ全体の歯車が止まる。
これを防ぐためには、
- タスク内の処理をさらに細かく分割する(ステートマシンパターンの導入)
- 外部プロセスの非同期実行 (`WshShell.Run` の第2引数・第3引数の活用) を組み合わせる
といった設計上のアプローチが必要となる。
③ エラーハンドリングの隔離
イベントループ内で稼働する個別のタスク(`Job_OutputLog` など)で予期せぬエラー(ファイルが存在しない、DB接続タイムアウト等)が発生したとき、`On Error Resume Next` のスコープが適切に設定されていないと、ループ全体がクラッシュしてスクリプトが死亡する。
各タスクのプロシージャ内では必ず局所的なエラーハンドリング(`On Error Resume Next` と `Err.Number` の監視)を徹底すること。
—
総括
`WScript.Sleep` は手軽だが、それはプログラマの思考停止の証明に他ならない。
「全体を止めるな、動かしながら調停せよ」。この原則を守るだけで、VBScriptで作る自動化ツールは、玩具レベルから「現場を支える堅牢な常駐システム」へと昇華する。
レガシーな言語だからこそ、アーキテクトの知見で美しく、かつ強靭に書く。そのこだわりが、あなたの業務を真に自動化する。
