hnwの日記

PHPのsleep関数とusleep関数の挙動を調べてみた

筆者はPHPの現在時刻を上書きするPHP拡張モジュールphp-timecopを開発しているため、PHPの時間がらみのテストを世間一般の人より多く書いていると思います。テストケース中でusleep関数を多用しているのは世界中でも筆者くらいかもしれません。

ところで、先日php-timecopのテストをWindows上で動かしたところ、 usleep(100000) が99.8msくらいで帰ってきてテストに失敗するということがありました。

筆者はsleep関数やusleep関数は指定した時間と同じかそれより長い時間スリープすると考えていたのですが、本当にそのような性質があるのでしょうか?また、sleep関数やusleep関数はどの程度の誤差があるのでしょうか?

本稿ではこうしたsleepやusleepの挙動について深掘りしてみます。

sleep関数の挙動

まずはsleep関数の挙動から調べてみましょう。LinuxmacOSWindowsの各環境で sleep(1) して帰ってくるまでの時間を1000回測定したときの結果を下記に示します。

Linuxの場合

下図がLinux環境(Debian 8.7, Kernel 3.16.0, x86_64, PHP 5.6.30)での実験結果のヒストグラムです。横軸の単位はusです。

1.0001秒あたりにピークがあるのがわかります。言い換えると約100usのズレです。

Windowsの場合

Windows環境(Windows 10, x86_64, PHP 7.1.6)での実験結果のヒストグラムです。

ピークは1.001秒あたりになっています。言い換えると約1msのズレです。

macOSの場合

macOS環境(Mac OS X 10.11.6, x86_64, PHP 7.1.6)での実験結果のヒストグラムです。

ピークは1.005秒あたりになっています。言い換えると約5msのズレです。

3環境とも、1秒より早く帰ってきたものは1件もありませんでした。

POSIXの仕様を確認する

ちなみに、PHPのsleep関数はどの環境でもOS/標準ライブラリのsleepを呼び出しています。POSIXのsleep()のmanpageには次のような記述があります。

The sleep() function shall cause the calling thread to be suspended from execution until either the number of realtime seconds specified by the argument seconds has elapsed or a signal is delivered to the calling thread and its action is to invoke a signal-catching function or to terminate the process. The suspension time may be longer than requested due to the scheduling of other activity by the system.

http://pubs.opengroup.org/onlinepubs/9699919799/functions/sleep.html

指定された時間が経過するまで実行を停止するよ、スケジューリングの都合で指定されたよりも長く停止することがあるよ、とのことですから、指定時間より早く帰ってくることはないことが保証されているわけです。

usleep関数の挙動

次にusleepの傾向を調べてみます。sleep関数と同様に3環境でusleep(1000000)して帰ってくるまでの時間を測定しました。

Linux/macOSの場合

LinuxmacOSの場合はsleep(1)のときと大差ない結果になりました。下記のヒストグラムLinuxの結果です。

以下はmacOSの結果です。

どちらの環境もsleep関数のときと同様、1秒より早く帰ってくるものは1件もありませんでした。両OSとも内部的にPOSIX準拠のusleepを呼び出しているため、sleep関数のときと似た挙動なのは当然と言えそうです。

Windowsの場合

さて、Windowsの場合は意外とも言える結果になりました。

コブが2つある分布になっており、最初のピークは1000000ns(=1秒)より僅かに前になっています。本稿の最初でWindows上のPHPではusleep関数が指定した時間より少し早く帰ってくることがあるという話を紹介しましたが、その通りの結果になっているわけです。

PHPWindows用のソースコードを見てみると、usleep関数の実現にはWindowsの「Waitable Timer Objects」が利用されています。また、これを利用すると停止時間が指定より短くなることがあるようです(たとえば下記の記事を参照)。

まとめ

  • PHPのusleep関数はWindows上では指定された時間より短い停止時間になることがある
    • Windows APIを使った独自実装をしているため
    • 手元の環境では最大1ms程度早まった(仮想環境ではさらにズレる印象)
  • 全ての環境のsleep関数・Linux/macOS上のusleep関数は指定された時間と同じかそれ以上停止する
    • 内部的に呼び出しているライブラリコールsleep・usleepの仕様
    • 指定された秒数からどれくらいズレるかはOSによって異なる

usleepという関数名なのにWindows上の挙動が他環境と異なっているのはバグとまでは言い切れませんが、ポータビリティ上は問題がありそうです。念のためPHP本体にバグレポートを出しておこうと思います。

追試用の情報

今回の実験に使ったPHPスクリプトは下記です。カーネルコンパイルオプションやその他の条件次第で結果が変わるかと思います。