えくせるちゅんちゅん

ことりがエクセルをちゅんちゅんするブログ

VBAのApplication.WaitとSleepの違い

よく Excel VBAApplication.Wait と WindowsAPIの Sleep は何が違うんですか?という質問をされることがあります。これは、疑問に思うのは無理もありません。結果だけ見れば「待つ」という同じ機能に見えます。

しかし、両者は全く異なったアプローチによって「待つ」実現しており、それぞれに適した使い方があります。

この記事では、筆者の個人的な経験と推測に基づいて、両者の違いを比較・解説していきます。

一般的な使用例

解説へ入る前に、一般的なWaitとSleepの書き方を紹介しておきます。

※コード中の Rem はコメントです。

Application.Wait の書き方

Wait は前準備せずに使用できます。※Access,Word,PowerPoint等では参照設定にExcelの追加が必要

Rem 今日の22時44分00秒まで待機する
Application.Wait "22:44:00"

Rem 0秒~1秒待機する
Application.Wait Now() + TimeValue("00:00:01")

Rem 1秒待機する
Application.Wait [Now() + "00:00:01"]

Rem 0.1秒待機する
Application.Wait [Now() + "00:00:00.1"]
Application.Wait [Now()] + 0.1 / 86400

Rem 22時44分00秒まで待機し待機したならTrue、ESCで強制終了されたならFalseを出力
Debug.Print Application.Wait("22:44:00")

WindowsAPI Sleepの書き方

Sleepはモジュールの先頭に次の記述が必要です。(Publicにするかはおまかせ)

Private Declare PtrSafe Sub Sleep Lib "kernel32.dll" (ByVal dwMilliseconds As Long)

実際に使用するときは、次のように記述します。

Rem 1秒待機する
Sleep 1000

Rem 0.1秒待機する
Sleep 100

WaitとSleepの違い

両者を比較すると次の表のようになります。

何故、表のような違いが生じているのか、原理から紐解いていきます。

WaitとSleepの待機は真逆の原理で実現されている

WindowsAPI Sleepとは

Sleep関数は、Windows OSが提供しているWindowsAPI Kernel32.DLL に含まれている関数の一つです。

WindowsAPIは、Windowsを構成する基本的なプログラムであり、どのWindowsパソコンでも使用できます。

Sleep関数に関して、 Microsoft 公式のDocs には次のように記載されています。

タイムアウト間隔が経過するまで、現在のスレッドの実行を一時停止します。

「スレッドの実行を一次停止する」というのは、OSという上位の存在によってVBAの流れを止められて、VBAからは何も出来なくなるということを意味しています。

いわばアプリケーションが眠っているような状態なので、演算能力(CPU)を必要としません。

「n秒後に鳴り響くキッチンタイマーを目覚まし時計に使う」というイメージが近いと思います。

VBAは基本的にシングルスレッドで動いているので、「スレッドが止まる」とは即ち「VBAがフリーズする」ことを意味します。

さらにVBAが動いている間は、「ベースのアプリケーション(Excel等)も使えなくなる」仕様なため、アプリがまるごとフリーズしたかの様な状態になります。

Sleep関数による待機状態は、引数で指定した時間が経過しない限り再開することはありません。

スリープから目覚めるためのコードを書けたとしても、そもそもプログラムが動いていないので、そのコードが実行されることはないのです。

夢を見てるときに、それを夢と自覚できても、自力で目覚められない筆者と同じです。

また、Docsには、こうも書いてあります。

警告可能な待機状態に入るには、 SleepEx関数を使用します。

このように、外部のスレッドを通してOSから強制的に目覚めさせることはできるようですが、シングルスレッドであるVBAからSleepEx関数を呼び出すのは難しいので、今回は出来ないものとして話を進めます。

Application.Waitとは

Application.Waitは、Excelライブラリに含まれているメソッド(=関数)の一つです。

VBAと言えばExcelVBAであることが多いため、頻繁にWaitが使われている機会を目にしますが、実はAccessPowerPointで使用できず、参照設定にExcelを加えるなどの準備が必要となります。

Microsoft 公式のDocs には、次のように記載されています。

実行中のマクロを指定の時刻まで停止します。 指定の時間に達した場合、True を返します。

こちらは、Sleepとは違い「指定の時間に達した場合に再開」です。

更にFalseを返す場合があるということから、待機状態の取り消しも想定内であることも分かります。

なんだか、Sleepと比べて優秀すぎる気がしますが、そんな甘い話ではありません。

このような仕組みを実現するには、「現在の時刻は指定した時間を超えたか」「キャンセルされたか(ESCキーが押されたか)」の2点を、常時監視するプログラムになっていないといけません。

VBAはマルチスレッドでは動きませんので、Wait関数を呼び出した先のプログラムは、メインスレッドのまま無限ループによって実現されていると想像がつきます。

WaitのイメージをVBAで再現したのが、こんなコードです。※実際には動きません。

Function MyWait(再開日時 As Date) As Boolean
    Do
        If Now() = 再開日時 Then Exit Do
        If IsPressESC Then Exit Function
    Loop
    MyWait = True
End Function

無限ループしていると考えられる根拠として、タスクマネージャを開いてCPU使用率を見ると、ExcelのCPU使用率が増大していることが挙げれられます。

今のパソコンでは確認が困難ですが、2000年代のシングルコアCPUのパソコンでは、VBA実行中は(Wait中でも)CPU使用率が100%に張り付いて他の作業が一切できないのが一般的でした。このことからも天井知らずの処理をしていた事が分かります。

次の図が2014年発売の第四世代4コアCPUで検証した画像です。ハードウェアの進化によってパソコン全体への影響度は下がっていますが、明らかに異常な増え方が観測できます。

以上の説明から、Sleep関数は計算させないことでプログラムを止めていて、Wait関数は全力で計算することでプログラムを止めていることが、おわかり頂けたと思います。

※WaitがCPU使用率が増大するような実装で放置されてしまったのは本当に謎です

この性質に加えて、Waitの注意点を説明していきます。

Waitはユーザーからの割り込みが可能である

既に述べたように、Application.Wait は待機中にESCキーの入力を監視しているため、ユーザーの意思で止めることが可能です。

VBA開発者であれば、Ctrl+Breakキーによって中断(VBEを開いてデバッグモードに突入)することもできます。

コードはこんなイメージです。

Rem 22時44分00秒まで待機して、待機したならTrue、強制終了されたならFalseを出力
Debug.Print Application.Wait("22:44:00")

Rem 今から0~1秒後まで待機して、待機したならTrue、強制終了されたならFalseを出力
Debug.Print Application.Wait(Now() + TimeValue("00:00:01"))

※2つ目がなぜ「今から1秒」ではなく「今から0~1秒」なのかは、後ほど説明します。

戻り値によってキャンセルされたかどうか分かるようになっていて、指定された時間まで待機できたらTrue。キャンセルされた場合はFalseを返します。

このキャンセル機能は、必ずしも便利とは言えなくて、

  • ユーザーの操作で止まって欲しまう
  • 次のDoEventsが現れるまで全てのWaitが無効化されてしまう(たまに無効化されないこともある)
  • 開発者がESCキーの長押しでVBAを中断できなくなる

と言ったデメリットがあります。

最後の問題に関しては、本来の中断コマンド Ctrl+Pause/Break キーで止めることが出来るので、このショートカットを覚えておくことが重要となります。

詳しくは以下ツイートの動画の通りです。

Waitは「指定日時より後なら再開する」ではなく「指定日時を超えたら再開する」である

Waitは「指定日時を横断したときに待機状態が終わる」という仕様になっています。

「指定日時より後なら再開する」ではありません。

従って、過去の日時を指定してしまうと、延々に待ち状態が終わりません。

日付を省略した表記や、 Now() を加算するのを忘れやすいので注意して下さい。

実装のイメージとしては、 If Now() > 指定日時 Then Exit Do ではなく、 If Now() = 指定日時 Then Exit Do みたいな感じになっていると予想されます。

(ところで、日時は浮動小数点型で記憶しているはずなので、単純なイコールだと絶対に見逃すと思います。見逃さないということはもっと変なロジックでチェックしていそうです。だからCPUが100%になるのかもしれません。)

Wait Now+1秒 は 1秒待機ではない

Waitに渡す引数は、(後述する文字列でも指定できますが)シリアル値(浮動小数点型)で指定します。

VBAの標準構文だけの場合、このような書き方をよく見かけます。

Rem 0秒~1秒待機する
Application.Wait Now() + TimeValue("00:00:01")

Excelの機能を使って似たような式を書くと、こうなります。

Rem 1秒待機する
Application.Wait [Now() + "00:00:01"]

しかし、この2つの挙動はコメントに書いた通り別物です。

実際に動かすと分かりますが、前者は実行の度に待ち時間に 0秒~1秒のブレがあります。後者はブレがありません。

この違いの原因は、ミリ秒にあります。

VBAのNowはミリ秒精度ではない

VBANow() は、 VBA.DateTime.Now() の略称ですが 2022/02/06 23:10:56 のようなデータを返します。

一方で、上記の角括弧を使った記述 [Now()] は、Application.Evaluate("Now()") の、特別な省略表記で、カッコ内をExcelの数式として評価するという関数で、ようするに ExcelワークシートのNow関数を実行した結果を返しています。

普通に確認するとVBAと同じ 2022/02/06 23:10:56 を返しますが、Excel関数のNowはミリ秒の精度を持っています。

本当かどうかは、両者を比較する式をイミディエイトで実行することで確認できます。

Rem Nowのミリ秒差を確認する式
Rem (ExcelNow - VbaNow)*24時間*60分*60秒
?([Now()]-Now()) * 86400
0.170000083744526 
0.73000006377697 
0.310000078752637 
0.839999970048666 
0.690000155009329

以上の通り、VBAの言語仕様によって記述されたWaitは、 VBA.Now() でミリ秒が取得できていないために、安定して 今から1秒先 の数値を指定できておらず、ブレが生じていたわけです。

VBAのTimeValueではミリ秒を加算できない

Waitのよくあるユースケースとして、「1秒も待ちたくない。0.1秒だけ待ちたい」みたいな場面があります。

しかし、VBAのTimeValueでは1秒未満の数値を書くことができません。

代わりに、以下のような書き方をすることで、解決することができます。

Rem 0.1秒待機する
Application.Wait [Now() + "00:00:00.1"]
Application.Wait [Now()] + TimeValue("00:00:01") / 10
Application.Wait [Now()] + 0.1 / 24 / 60 / 60
Application.Wait [Now()] + 0.1 / 86400

1つ目は0.1秒の表現をEvaluateでExcelの数式に任せる方法。残りは計算によってミリ秒へ変換する方法です。

ミリ秒を変数等で指定することも視野に入れると、3か4つ目が無難ではないかと考えられます。

なお、Waitの為にはExcelの参照設定は必須ですし、Nowの為にはEvaluateを使うことになるので、定型文で良ければ1つ目も選択肢に入るかと思います。

Waitで時刻の文字列指定はしてはいけない

Waitの引数には、いきなり文字列を指定することもできます。

Application.Wait "22:44:00" 

この書き方はバグを起こしやすいので、絶対に使ってはいけません。

Waitの仕様により、過去の日時が指定されたとき、永遠に待機状態が終わりません。

しかも、現実の時間が 2022/1/1 23:30:00 の時に 22:44:00 を渡したとき、 2022/1/1 22:44:00 として解釈されるため、意図せずして過去の日時を指定してしまう状況が必ず起こります。

Sleepで大きな値を渡すと安全に中断できなくなる

Sleep中はESCも、Ctrl+Breakも、停止ボタンも、何も効きません。

1秒や10秒ならまだ大丈夫ですが、100秒や1000秒を渡そうものなら、本気でその時間数待たないと行けません。

たとえば、Waitと間違えて Now を渡した場合、 2022/3/2844648 ですから45秒待つ必要があります。たぶん、まさかそんなことになると思っていないので、フリーズしたとドキドキすると思います。

1万,10万を渡そうものなら、待つなんて無理ですから、Excelをタスクマネージャから強制終了させることになるかと思います。

そのため、頻繁に使うのであれば、Sleepをラップする関数を自作して、適度に制御をVBAに返すようにすると良いかもしれません。

低負荷でESCデバッグ割り込み可能なSleepラップ関数

一例ですが、次のようなコードを使えば、Do~Loopの中で中断させることが可能となります。

Option Explicit

Declare PtrSafe Sub Sleep Lib "kernel32" (ByVal dwMilliseconds As Long)

Sub SleepCPU低負荷でESCデバッグ対応(ミリ秒 As Long, Optional 待ち時間精度 = 10)
    Dim 終了日時: 終了日時 = [Now()] + CDbl(ミリ秒) / 1000# / 86400#
    Do
        If [Now()] >= 終了日時 Then Exit Do
        Call Sleep(待ち時間精度)
    Loop
End Sub

Sub test()
    Dim tStart As Single, tStop As Single: tStart = Timer
    SleepCPU低負荷でESCデバッグ対応 1000
    tStop = Timer: Debug.Print Format$(tStop - tStart, "0.00") & " sec."
End Sub

低負荷でESCによる待ち時間取り消しに対応したSleepラップ関数

ついでのついでに、WaitのようにESCキーで待ち状態をキャンセルして、かつデバッグが表示されない関数はこんな感じです。

※ただしCtrl+Breakによる中断ができません。

Option Explicit

Declare PtrSafe Sub Sleep Lib "kernel32" (ByVal dwMilliseconds As Long)

Declare PtrSafe Function GetAsyncKeyState Lib "User32.dll" (ByVal vKey As Long) As Integer

Rem ESCキーが押されているか?
Function IsPressESC() As Boolean
    Const KEY_PRESSED = -32768
    IsPressESC = (GetAsyncKeyState(vbKeyEscape) And KEY_PRESSED) = KEY_PRESSED
End Function

Function SleepCPU低負荷でESC加速対応(ミリ秒 As Long, Optional 待ち時間精度 = 10) As Boolean
    SleepCPU低負荷でESC加速対応 = True
    Dim 終了日時: 終了日時 = [Now()] + CDbl(ミリ秒) / 1000# / 86400#
    
'    Application.Interactive = False
    Application.EnableCancelKey = xlDisabled
    
    Do
        If [Now()] >= 終了日時 Then Exit Do
        Call Sleep(待ち時間精度)
        If IsPressESC Then SleepCPU低負荷でESC加速対応 = False: Exit Do
    Loop
    
'    Application.Interactive = True
    Application.EnableCancelKey = xlInterrupt
End Function

Sub test()
    Dim tStart As Single, tStop As Single: tStart = Timer
    Debug.Print SleepCPU低負荷でESC加速対応(5000)
    tStop = Timer: Debug.Print Format$(tStop - tStart, "0.00") & " sec."
End Sub

まとめ

というわけで、まとめです。

Application.Waitのメリット

  • ユーザーはESCキーで待機を0にできる
  • 開発者はCtrl+BreakキーでVBAが中断ができる

Application.Waitのデメリット

  • 過去の日時を指定すると永遠に終わらない
  • ユーザーはESCキーで待機を0にできてしまう

Sleepのメリット

  • PCへの負荷がない
  • キー入力で中断される心配がない

Sleepのデメリット

  • Declare宣言が面倒
  • 長大な待ち時間を指定すると復旧できない
  • スリープ中は一切の操作を受け付けてくれない

身も蓋もない言い方

要は ・Waitはペットに「待て!」をして食い止めてる状態 ・Sleepはペットに麻酔注射して眠らせてる状態 ってことです。

参考

本記事は以下の連続ツイートを元に執筆しました。

謝辞

このお題は、日頃からTwitterでも偶に見かける疑問の一つですが、本格的に使ってみないと違いを実感することができず、軽く説明して理解を得づらいのが難点でした。

先日のVBAer達が集まるVBA会で、ちょっと説明させていただく機会があり、考えを整理することができたので、このような文章化に至りました。

改めてお礼申し上げます。いつも楽しい場を提供していただき、ありがとうございます。

本記事で登場した角括弧(Evaluate)の制限等に関しては、以下の記事をご覧下さい。

www.excel-chunchun.com

無限ループからの回復については、こちらの記事をご覧下さい。

www.excel-chunchun.com


それではまた来週♪ちゅんちゅん(・8・)