コード遅延(スリーピングとも呼ばれる)は、その名の通り、コードの実行を一定時間遅延させることです。
コード遅延の最も一般的なニーズは、他のプロセスが終了するのを待ち、そのプロセスの結果で作業できるようにするときです。
マルチスレッドシステムでは、あるスレッドは他のスレッドが処理を終えるのを待ち、その結果で作業を続けたいと思うかもしれません。
他の例としては、作業中のサーバーにかかる負担を軽減することが挙げられます。
例えば、Web スクレイピングを (倫理的に) 行っていて、問題の Web サイトの ToS に従い、 robots.txt
ファイルを遵守しているとき、サーバーのリソースを圧迫しないように各リクエストの実行を遅らせたいと思うかもしれません。
多くのリクエストが連続して発生すると、サーバーによっては、すぐにすべての空きコネクションを占有し、事実上DoS攻撃となる可能性があります。
そこで、Webサイトの利用者やWebサイト自体に悪影響を与えないように、1つ1つのリクエストに遅延を与えて送信数を制限します。
試験の結果を待っている学生は、学校のウェブサイトを猛烈に更新して、ニュースを待っているかもしれません。
あるいは、Webサイトに何か新しい情報があるかどうかをチェックするスクリプトを書くかもしれません。
ある意味で、コードの遅延は、有効なループと終了条件があれば、技術的にはコードのスケジューリングになり得ます – 適切な遅延メカニズムがブロックしていないと仮定すれば。
この記事では、Pythonでコードの実行を遅延させる方法(スリーピングとしても知られています)を見ていきます。
time.sleep() でコードを遅延させる
この問題に対する最も一般的な解決策のひとつが、組み込みの time
モジュールの sleep()
関数です。
他の多くの言語がミリ秒単位であるのとは異なり、この関数ではプロセスを何秒間スリープさせたいかを指定します。
import datetime
import time
print(datetime.datetime.now().time())
time.sleep(5)
print(datetime.datetime.now().time())
この結果は
14:33:55.282626
14:34:00.287661
はっきり言って、2つの print()
ステートメントの間に5秒の遅延があることがわかりますが、これは小数点以下2桁までかなり高い精度です。
もし、1秒未満のスリープが必要なら、整数でない数字も簡単に渡すことができます。
print(datetime.datetime.now().time())
time.sleep(0.25)
print(datetime.datetime.now().time())
14:46:16.198404
14:46:16.448840
print(datetime.datetime.now().time())
time.sleep(1.28)
print(datetime.datetime.now().time())
14:46:16.448911
14:46:17.730291
特に print()
文の実行には(可変)時間がかかるので、テストするのが難しいということに注意してください。
しかし、time.sleep()
関数には、マルチスレッド環境で顕著に表れる、1つの大きな欠点があります。
time.sleep()はブロックされます。
これは、それが乗っているスレッドを押収し、スリープの間、それをブロックします。
このため、待ち時間が長いと、その間にプロセッサのスレッドが詰まってしまい、不向きです。
また、非同期アプリケーションや反応型アプリケーションでは、リアルタイムのデータやフィードバックが必要になることが多いので、このようなアプリケーションには不向きです。
もう一つ time.sleep()
について注意すべき点は、止めることができないという事実です。
一度開始すると、プログラム全体を終了させるか、sleep()
メソッド自体が例外を発生させて停止させない限り、外部からはキャンセルできません。
非同期プログラミングと反応プログラミング
非同期プログラミング
非同期プログラミングは、メインフローとは別にタスクを実行し、終了させることができる並列実行が中心です。
同期プログラミングでは、関数Aが関数Bを呼び出すと、関数Bの実行が終了するまで実行が停止し、その後関数Aが再開することができます。
非同期プログラミングでは、A関数がB関数を呼び出す場合、B関数からの結果に依存するしないにかかわらず、両者が同時に実行でき、必要であれば、もう一方の関数が終了するまで待ち、互いの結果を利用することができる。
Reactive ProgrammingはAsynchronous Programmingのサブセットで、データが提示されると、それを処理するはずの関数がすでに忙しいかどうかに関係なく、反応的にコードを実行するトリガーとなる。
Reactive Programmingは、メッセージ駆動型アーキテクチャ(メッセージは通常イベントまたはコマンド)に大きく依存している。
非同期アプリケーションも反応型アプリケーションも、ブロックされたコードに大きく悩まされるものです。
したがって、time.sleep()
のようなものを使うことは、これらのアプリケーションには適していません。
それでは、ノンブロッキングなコードの遅延オプションを見てみましょう。
asyncio.sleep() によるコードの遅延
Asyncio は並列コードを書くための Python ライブラリで、 async
/await
構文を使っています。
これは他の言語で使ったことがある開発者にとっては馴染みがあるかもしれません。
このモジュールを pip
経由でインストールしましょう。
$ pip install asyncio
インストールしたら、スクリプトに import
して、関数を書き換えてみましょう。
import asyncio
async def main():
print(datetime.datetime.now().time())
await asyncio.sleep(5)
print(datetime.datetime.now().time())
asyncio.run(main())
asyncioを使用する場合、非同期に実行される関数を
asyncとしてマークし、
asyncio.sleep()などの将来のある時点で終了する処理の結果を
await` します。
前の例と同様に、これは5秒間隔で2回プリントします。
17:23:33.708372
17:23:38.716501
しかし、これでは asyncio.sleep()
を使う利点がよくわかりません。
この例をいくつかのタスクを並列に実行するように書き換えてみましょう。
そうすれば、この区別はもっと明確になります。
import asyncio
import datetime
async def intense_task(id):
await asyncio.sleep(5)
print(id, 'Running some labor-intensive task at ', datetime.datetime.now().time())
async def main():
await asyncio.gather(
asyncio.create_task(intense_task(1)),
asyncio.create_task(intense_task(2)),
asyncio.create_task(intense_task(3))
)
asyncio.run(main())
ここでは async
関数を用意し、5秒で終わるような手間のかかるタスクをシミュレートしています。
そして、asyncio
を使って、複数のタスクを作成します。
各タスクは非同期に実行できますが、それは非同期に呼び出した場合のみです。
もし、順次実行するのであれば、タスクも順次実行される。
並列に呼び出すには、 gather()
関数を使用します。
1 Running some labor-intensive task at 17:35:21.068469
2 Running some labor-intensive task at 17:35:21.068469
3 Running some labor-intensive task at 17:35:21.068469
これらはすべて同時に実行され、3つのタスクの待ち時間は15秒ではなく、5秒になります。
一方、このコードをいじって、代わりに time.sleep()
を使うようにすると、次のようになります。
import asyncio
import datetime
import time
async def intense_task(id):
time.sleep(5)
print(id, 'Running some labor-intensive task at ', datetime.datetime.now().time())
async def main():
await asyncio.gather(
asyncio.create_task(intense_task(1)),
asyncio.create_task(intense_task(2)),
asyncio.create_task(intense_task(3))
)
asyncio.run(main())
各 print()
文の間に 5 秒間待機することになります。
1 Running some labor-intensive task at 17:39:00.766275
2 Running some labor-intensive task at 17:39:05.773471
3 Running some labor-intensive task at 17:39:10.784743
タイマーを使ったコードの遅延
Timerクラスは
Threadで、ある一定の時間が経過した後にのみ処理を実行することができます。
この動作はまさに私たちが求めているものです。
しかし、まだマルチスレッドシステムを使用していない場合、Thread` を使用してコードを遅延させるのは少しやりすぎです。
Timerクラスは
start()を必要とし、
cancel()` で停止させることができる。
コンストラクタには、2番目のパラメータである関数を実行する前に待つ秒数を表す整数を渡します。
それでは、関数を作って Timer
を使って実行してみましょう。
from threading import Timer
import datetime
def f():
print("Code to be executed after a delay at:", datetime.datetime.now().time())
print("Code to be executed immediately at:", datetime.datetime.now().time())
timer = Timer(3, f)
timer.start()
この結果は
Code to be executed immediately at: 19:47:20.032525
Code to be executed after a delay at: 19:47:23.036206
cancel()` メソッドは、複数の関数を実行していて、ある関数の実行結果や条件によって、その関数の実行をキャンセルしたい場合にとても役に立ちます。
f2()と
f3()の両方を呼び出す関数
f()を書いてみましょう。
f2() はそのまま呼び出され、1 から 10 までのランダムな整数を返し、その関数を実行するのにかかった時間をシミュレートします。
f3()は
Timerを通して呼ばれ、
f2()の結果が
5よりも大きければ
f3()はキャンセルされる。
一方、f2()が
5以下の時間で動作した場合には
f3()` はタイマーが切れた後に実行される。
from threading import Timer
import datetime
import random
def f():
print("Executing f1 at", datetime.datetime.now().time())
result = f2()
timer = Timer(5, f3)
timer.start()
if(result > 5):
print("Cancelling f3 since f2 resulted in", result)
timer.cancel()
def f2():
print("Executing f2 at", datetime.datetime.now().time())
return random.randint(1, 10)
def f3():
print("Executing f3 at", datetime.datetime.now().time())
f()
このコードを複数回実行すると、以下のようになります。
Executing f1 at 20:29:10.709578
Executing f2 at 20:29:10.709578
Cancelling f3 since f2 resulted in 9
Executing f1 at 20:29:14.178362
Executing f2 at 20:29:14.178362
Executing f3 at 20:29:19.182505
イベントによるコードの遅延
Eventクラスは、イベントを生成するために使用することができる。
1つのイベントを複数のスレッドで “listen” することができます。
Event.wait() 関数は、 Event.isSet()
が設定されていない限り、それが実行されているスレッドをブロックする。
いったんイベントを set()
したら、待機していたすべてのスレッドが呼び出され、 Event.wait()
はノンブロッキングになります。
これはスレッドの同期に使うことができます。
すべてのスレッドが積み重なって、あるイベントがセットされるまで wait()
を行い、その後、スレッドのフローを決めることができます。
例えば、 waiter
メソッドを作成して、それを異なるスレッドで複数回実行してみましょう。
各ウェイターはある時間になると作業を開始し、1秒ごとにまだ時間内かどうかをチェックし、注文を受ける直前に、その注文を満たすために1秒かかるようにします。
彼らはイベントが設定されるまで、つまり作業時間が終了するまで働き続ける。
各ウェイターは自分のスレッドを持ち、管理者はメインスレッドに常駐して、みんなが家に電話をかけられるようになったら電話をかける。
今日は気前がいいので、作業時間を短縮し、4秒働いたら帰らせることにします。
import threading
import time
import datetime
def waiter(event, id):
print(id, "Waiter started working at", datetime.datetime.now().time())
event_flag = end_of_work.wait(1)
while not end_of_work.isSet():
print(id, "Waiter is taking order at", datetime.datetime.now().time())
event.wait(1)
if event_flag:
print(id, "Waiter is going home at", datetime.datetime.now().time())
end_of_work = threading.Event()
for id in range(1, 3):
thread = threading.Thread(target=waiter, args=(end_of_work, id))
thread.start()
end_of_work.wait(4)
end_of_work.set()
print("Some time passes, management was nice and cut the working hours short. It is now", datetime.datetime.now().time())
このコードを実行すると、次のようになる。
1 Waiter started working at 23:20:34.294844
2 Waiter started working at 23:20:34.295844
1 Waiter is taking order at 23:20:35.307072
2 Waiter is taking order at 23:20:35.307072
1 Waiter is taking order at 23:20:36.320314
2 Waiter is taking order at 23:20:36.320314
1 Waiter is taking order at 23:20:37.327528
2 Waiter is taking order at 23:20:37.327528
Some time passes, management was nice and cut the working hours short. It is now 23:20:38.310763
end_of_work` イベントは、2つのスレッドを同期させ、いつ作業し、いつ作業しないかを制御し、チェックの間に設定した時間だけコードの実行を遅らせるために使用されています。
結論
このガイドでは、Pythonでコードの実行を遅延させるいくつかの方法を見てきました – それぞれ異なるコンテキストや要件に適用できます。
通常の time.sleep()
メソッドはほとんどのアプリケーションでかなり有用ですが、長い待ち時間にはあまり最適ではなく、単純なスケジューリングにはあまり使われませんし、ブロックされます。
asyncioを使うことで、
time.sleep()の非同期版を手に入れ、
await` することができるようになりました。
Timer` クラスはコードの実行を遅らせ、必要であればキャンセルすることができる。
Event` クラスは、複数のスレッドがリスニングできるイベントを生成し、それに応じて反応し、特定のイベントが設定されるまでコードの実行を遅延させます。