pythonのsleepでミリ秒単位で正確にプログラムを停止する

pythonのsleep関数を使ったとき、指定した時間より長い間停止してしまうことはないでしょうか?sleep関数は指定した時間、プログラムを停止する関数ですが、ミリ秒単位の短い時間を指定した場合に意図したよりも長い間止まってしまうことがあります。

この記事では、Windowsでpythonのsleep関数で、より正確に精度よくプログラムを停止する方法を紹介します。特に、ミリ秒(msec)単位の短い時間プログラムを停止する際に重要となる方法です。

この記事は前置き等が長いので、より正確にプログラムを停止する方法をすぐに知りたい方は、目次から「4. sleepで1ミリ秒の精度でプログラムを停止する」に飛んでください。

この記事でできること

Windowsのsleep関数でミリ秒(msec)単位の短い期間プログラムを停止する際、停止時間が思ったよりも長い時間となったり、sleep関数が実行されるたびに大きくばらついたりすることがあります。

sleep関数は、例えば映像関係のAIのデモプログラムとか、実時間が重要なプログラムを作成するときに使うことがあります。ミリ秒単位でsleepで停止させて映像等の見栄えを調整する際、sleepの停止時間のばらつきで困惑することがあるかもしれません。

正確性を無視してざっくり言うと、デフォルト設定のWindowsでは約15ミリ秒(msec)以下の時間をsleepで停止することはできません。少なくとも、ばらつきなく安定して約15ミリ秒(msec)以下の停止はできません。

この記事では、約15ミリ秒(msec)よりも短い時間プログラムを停止する方法を紹介します。

sleep関数の使い方と停止に使われるOSタイマーについて

sleep関数の基本的な使い方と、sleep関数でのプログラムの停止に使われているOSタイマーについて説明します。

pythonのsleep関数の使い方

pythonのsleep関数はtimeモジュールのメソッドです。

引数に停止したい時間(単位は秒[sec])を指定します。1秒よりも短い時間停止したい場合は、次のサンプルコードのように小数を指定すればOKです。

import time
time.sleep(0.001) # 0.001秒(=1ミリ秒)待つ

sleep関数が記述された行で、引数に指定した時間だけプログラムが停止します。

OSのタイマーについて

sleep関数が何故正確に停止できないことがあるのか、その原因としてはOSの制約があります。この記事ではOSとしてWindowsを扱いますが、OSにはタイマーというものがあり、sleep関数はOSタイマーの精度に左右されることが多々あります。

sleep()関数等の時間に関係する処理を行う際、タイマー精度が悪いと正確な時間を計測できず(そもそもOSが正確に時間を計測していない)、意図しない時間間隔で処理されてしまいます。

例えば、sleep()関数で1ミリ秒(msec)の間プログラムを停止したくても、1ミリ秒(msec)がタイマーの最小単位(精度)よりも小さい場合には、1ミリ秒(msec)よりも長い時間、プログラムは停止してしまいます。

Windowsのタイマー精度は、defaultでは15.625(ミリ秒)msecとされています。正確には、OSの細かい仕様で色々ありますが、少なくとも15msec以下の時間をsleepで停止したくても正確な時間を期待できないということです。

Windowsでタイマー間隔を設定する

では、windowsで15.625ミリ秒(msec)よりも短い時間をsleepで停止できないかというと、そうではなく、windowsのAPIを使ってOSタイマーを変更することができます。

OSタイマーを変更するには関数”windll.winmm.timeBeginPeriod()”を使用します。使い方は、次の使用例の通りです。


    from ctypes import windll
    # タイマー精度を1msec単位にする
    windll.winmm.timeBeginPeriod(1)

    ### 1msec単位で処理したい関数(sleep関数等)を記述

    # タイマー精度を戻す
    windll.winmm.timeEndPeriod(1)

ただし注意点があります。Windowsのタイマー精度を変更するのは他のプログラム等に大きな影響を及ぼす可能性があります。タイマー精度の変更が必要な処理が終わったら、タイマー精度をdefaultの値に戻すことを強く推奨されています。

上のサンプルコードのように、sleep関数等のOSタイマー精度の変更が必要な処理が終わったら、関数”windll.winmm.timeEndPeriod()”でOSタイマー精度を戻すという処理が必要です。

参考URL: https://docs.microsoft.com/en-us/windows/win32/api/timeapi/nf-timeapi-timebeginperiod

Windowsのタイマーの設定関数”windll.winmm.timeBeginPeriod()”を使うには、”from ctypes import windll”でwindllをimportします。”windll.winmm.timeBeginPeriod()”の引数がタイマー精度です。引数はunsignedの整数で、単位はミリ秒(msec)です。

タイマー精度をWindowsのdefaultに戻す関数”windll.winmm.timeEndPeriod()”の引数には、windll.winmm.timeBeginPeriod(period)で指定した値(period)を指定してください。

上のコードでは、”windll.winmm.timeBeginPeriod(1)”と1ミリ秒(msec)を引数で指定したので、”windll.winmm.timeBeginPeriod(1)”の引数も”1″としています。

sleepの停止時間の計測(デフォルト)

では、実際にsleepを実行し、停止時間を計測していきます。

まずは、pythonプログラムの計算時間を計測する方法を説明します。一般的なプログラムの計算時間の計測にも使える方法です。

pythonでプログラムの処理時間を計測する方法

pythonでのプログラムの計算時間を計測する方法は色々ありますが、この記事では”time.perf_counter()”を使用します。

次のサンプルコードでは”sleep(0.001)”の処理時間を計測しています。プログラムの処理時間を計測したい箇所の前後に”time.perf_counter()”を記述し、取得された変数の差をとると、その値が計測された処理時間となります。

data=[]
start = time.perf_counter() # 時間計測スタート
time.sleep(0.001) # 0.001秒(=1ミリ秒)待つ
end = time.perf_counter() # 時間計測終わり

time_sec = end - start # スタートから終わりまでの時間[秒]
data.append(time_sec*1000) # ミリ秒で配列dataに追加

このサンプルコードでは、変数”time_sec”が計測時間で、単位は秒(sec.)となります。このコードでは”time_sec”を1000倍して単位をミリ秒(msec)としました。

pythonでのプログラムの処理時間の計測方法は色々あり、どのモジュールを使った計測方法がより正確に精度よく時間を計測できるのか議論があるところですが、ここでは正確性が高いとされる”time.perf_counter()”を使用しました。

デフォルトのOSタイマー精度でsleepの停止時間を計測

まずはタイマーに関係する設定を何も行わないで、デフォルトの状態でsleep関数で停止される時間を計測してみます。次のサンプルコードでは、sleep()関数に1ミリ秒(msec)の停止時間を指定しています。

    import time
    
    data = [] # sleepの時間を格納する配列(リスト)の初期化
    # 1000回sleep(0.001)を実行
    for i in range(1000):
        start = time.perf_counter() # 時間計測スタート
        time.sleep(0.001) # 0.001秒(=1ミリ秒)待つ
        end = time.perf_counter() # 時間計測終わり
        time_sec = end - start # スタートから終わりまでの時間[秒]
        data.append(time_sec*1000) # ミリ秒で配列dataに追加
    
    print("Average time: ", sum(data)/len(data), " [msec]")
    
    import matplotlib.pyplot as plt

    # ヒストグラムの横軸。1刻み0~30の範囲の配列(リスト)
    bin_list10 = [i for i in range(31)]
    print("bin_list10", bin_list10)
    
    plt.hist(data, bins=bin_list10) # ヒストグラム生成
    plt.savefig("histogram_time10.png") # 画像として出力
    plt.clf() # 図をクリア

このコードでは、1ミリ秒の間停止する関数”time.sleep(0.001)”を1000回実行し、各実行時の時間をリストdataに保持してヒストグラムとして出力しています。matplotlibライブラリを使用したヒストグラムの作り方は次の過去記事で紹介しました。

得られたヒストグラムは次のようになります。1ミリ秒の間停止するはずの関数”time.sleep(0.001)”を実行したにも関わらず、約15ミリ秒の処理時間がかかりました。

デフォルトのタイマー精度でのsleep時間
デフォルトのタイマー精度でのsleep時間(横軸ミリ秒、1msec単位)

繰り返しになりますが、プログラマーの意図としては1ミリ秒であるに関わらず、実際には15ミリ秒程度の停止時間となってしまいました。

上述の通り、デフォルトのWindowsのタイマー精度は15.625(ミリ秒)msec単位とされていますので、そういう意味では妥当な結果であると言えます。

次に、OSタイマーを変更し、1ミリ秒精度でのsleepの停止を行う方法を紹介します。

sleepで1ミリ秒の精度でプログラムを停止する

次に、タイマー精度を1ミリ秒(msec)と設定した場合のsleep()関数の停止時間を計測してみます。

    import time
    from ctypes import windll

    # タイマー精度を1msec単位にする
    windll.winmm.timeBeginPeriod(1)

    data = [] # sleepの時間を格納する配列(リスト)の初期化
    # 1000回sleep(0.001)を実行
    for i in range(1000):
        start = time.perf_counter() # 時間計測スタート
        time.sleep(0.001) # 0.001秒(=1ミリ秒)待つ
        end = time.perf_counter() # 時間計測終わり
        time_sec = end - start # スタートから終わりまでの時間[秒]
        data.append(time_sec*1000) # ミリ秒で配列dataに追加
    
    print("Average time: ", sum(data)/len(data), " [msec]")
    
    
    # タイマー精度を戻す
    windll.winmm.timeEndPeriod(1)
    
    
    import matplotlib.pyplot as plt
    
    plt.hist(data, bins=bin_list10) # ヒストグラム生成
    plt.savefig("histogram_after_time10.png") # 画像として出力
    plt.clf() # 図をクリア    
    

上でも記載しましたが、関数”windll.winmm.timeBeginPeriod(1)”でタイマー精度を1ミリ秒(msec)に設定しています。また、”windll.winmm.timeEndPeriod(1)”で、タイマー精度をdefaultに戻すようにしています。

sleep関数の前後にこれらの関数を挟むことで、1ミリ秒単位の正確な停止を行うことができるというわけです。ただし、完璧に1ミリ秒の停止が実現できるというわけではありません。

このサンプルコードでも上で紹介したデフォルトのタイマー設定でのサンプルコード同様に、本来1ミリ秒停止するはずの関数”time.sleep(0.001)”を1000回実行し、処理時間をヒストグラムとして取得しました。

その結果が次の画像です。上のデフォルトのタイマー設定でのヒストグラムと比べると、かなり正確に計測できているのがわかると思います。

1ミリ秒精度のタイマーでのsleep時間
1ミリ秒精度のタイマーでのsleep時間(横軸ミリ秒、1msec単位)

上のデフォルトのタイマー設定では15msec周辺に棒グラフが偏っていたのとは明確に違って、1ミリ秒(msec)周辺に固まっていることがわかると思います。上に掲載したヒストグラムのグラフと比較してみて下さい。

タイマー精度を1ミリ秒(msec)に変更することで、意図せずに約15msecとなっていたsleepの停止時間を約1ミリ秒(msec)にすることができました。

ただし、正確にプログラム停止時間を1msecとできるわけではありません。

参考までに、このヒストグラムの横軸の0~3msecを拡大したヒストグラムを次に貼り付けておきます。

1ミリ秒精度のタイマーでのsleep時間(横軸0.1msec単位)
1ミリ秒精度のタイマーでのsleep時間(横軸ミリ秒、0.1msec単位)

1msecと2msecを中心に計測時間が集まっていました。sleep(0.001)は1ミリ秒(msec)の停止を行うはずであるのに関わらず、2ミリ秒(msec)にもピークがあります。

これは、sleep関数が実行されるタイミングや、OSの割込が時間計測関数”time.perf_counter()”にも影響しているのかなと。これは私の推測に過ぎません。

少なくとも、本記事の内容を参考にタイマー精度を変更したとしても、必ずしも正確に時間を計測できるとは限らないことには注意が必要です。

しかし、本記事の方法を行わないデフォルトの設定よりは正確にプログラムが停止できたと言えます。

おわりに

今回は、Windowsのタイマー精度の変更方法を紹介し、より正確にsleep関数でプログラムを停止する方法を紹介しました。変更したタイマー精度はデフォルトに戻すことも忘れずにお願いします。

あと注意点としては、この記事の方法でタイマー精度を変更したとしても、必ずしも正確な時間でプログラムを停止できるとは限りません。Windowsのシステム上、それは難しいです。

ただし、何もしないデフォルトの設定よりは正確に時間を計測できると思います。

蛇足ですが、正確な経過時間に基づいた処理を行うには、リアルタイムOSという特殊なOSやシステムが必要となります。

コメント

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