色々あったのでPythonでプロセスをまたいだ排他制御が出来ないか試しました。
今回プロセス間排他にはWindows APIのMutexを利用しました。
用語
クリティカルセクション
排他処理で必ずと言っていいほど出てくる用語がこのクリティカルセクション(Critical Section)です。
直訳すると、危険な範囲とか重要な範囲とかそういう感じです。辞書には危険領域と記述されていました。
プログラムの世界でクリティカルセクションとは一度に1つのシステムからだけで処理してもラわないと困るコード上の範囲です。
例えば、ログファイルへの記述を考えます。
- ユーザーが入力した内容
- 計算結果
の2つをログに残すとします。
疑似コードとしては
inputData = 入力値 print inputData #print1 computedData = inputData + 1 print computedData #print2
こんな感じとします。
この処理を複数のシステム(システムAとシステムBとします)から同時に呼び出すと、
システムAのprint1 ⇒ システムBのprint1 ⇒ システムAのprint2 ⇒ システムBのprint2
という流れになってしまう事がありえます。
こうなるとログの内容がしっちゃかめっちゃかで参考にならなくなってしまいます。
Mutex
Mutexはクリティカルセクションを守るための仕組みのことです。
Mutexを利用する事でクリティカルセクションを処理できるのが1つだけに限定することが出来ます。
Pythonではthreading.Lockというのがそれにあたるようですが、名前のとおり、スレッド間の排他は可能ですが、プロセスをまたぐと効果がありません。
Windows APIのMutex
Windows APIにはMutexを利用するものが用意されています。
このMutexはPythonのものと違いプロセスをまたいでいても有効です。
処理の流れはCreateMutexでMutexを作り、WaitForSingleObject~ReleaseMutexでクリティカルセクションを囲うだけです。
処理が終わったらCloseHandleでMutexを削除します。
Windows APIのMutexでは名前を付けることができ、その名前でクリティカルセクションを区別します。
Pythonで実装
# -*- coding: utf-8 -*- import time # criticalsection def criticalsection(): for i in range(10): print(i) time.sleep(0.5) criticalsection()
まずクリティカルセクションとなる部分を用意しました。
ただ、0.5秒ごとに数字を出すだけです。
もちろんこれを2つ同時に実行したら同時に数字が列挙されていきます。
Windows APIを使う為の準備
Windows APIを使う為にはctypesというライブラリを利用します。
import ctypes Kernel32 = ctypes.windll.Kernel32
Mutex関連はKernel32.dllに定義されているのでwindll.Kernek32を取得しています。
(これはMSDNの下の方にインポートライブラリ:○○.libという記述でわかります。)
Mutexを作る
MSDNにある通りに書いていきます。
mutex = Kernel32.CreateMutexA(0, 1, "mutex-unique-name")
セキュリティ記述子は不要なので0、所有者になりたいので次が1、最後にMutexの名前ですがが、これはなんでもよいです。同じ名前が指定されたものは同一のMutexとみなされます。
これだけでMutexが完成です。
Mutexで排他する(終わるのを待つ)
実際に排他するにはWiatForSingleObjectを利用します。
Kernel32.WaitForSingleObject(mutex, -1) # 排他が必要な処理 criticalsection() Kernel32.ReleaseMutex(mutex)
これだけです。
WiatForSingleObjectの1番目は作成したMutexを、2番目にはどれくらい待つかの時間を指定します。-1は無限に待ちます。
排他が必要な部分が終わったらReleaseMutexしてやります。
それを踏まえて全体です。
# -*- coding: utf-8 -*- import time import ctypes Kernel32 = ctypes.windll.Kernel32 # criticalsection def criticalsection(): for i in range(10): print(i) time.sleep(0.5) # Lock mutex = Kernel32.CreateMutexA(0, 1, "mutex-unique-name") Kernel32.WaitForSingleObject(mutex, -1) # ロック解除されるまで待つ criticalsection() Kernel32.ReleaseMutex(mutex) Kernel32.CloseHandle(mutex)
この状態で複数起動すると確かに、最初の方がおわるの待つと思います。
Mutexで排他する(終わるのは待たない)
さて次は終わるのは待たずに、別のプログラムがロックしていたらエラーを表示するようにします。
result = Kernel32.WaitForSingleObject(mutex, 0) # ロックされているかどうかだけ見る if result == 0x00000102: print("エラー:lock済") else: criticalsection() Kernel32.ReleaseMutex(mutex)
WaitForSingleObjectの2番目は待ち時間と書きました、-1だと無限に待ちますが、0だとまったく待ちません。
そして、排他に失敗すると0x00000102という数値を返します。(成功時は0なのでその判定もした方が良いかもしれません…)
以下全体です。
# -*- coding: utf-8 -*- import time import ctypes Kernel32 = ctypes.windll.Kernel32 # criticalsection def criticalsection(): for i in range(10): print(i) time.sleep(0.5) # Lock mutex = Kernel32.CreateMutexA(0, 1, "mutex-unique-name") result = Kernel32.WaitForSingleObject(mutex, 0) # ロックされているかどうかだけ見る if result == 0x00000102: print("エラー:lock済") else: criticalsection() Kernel32.ReleaseMutex(mutex) Kernel32.CloseHandle(mutex)
という感じです。
こちらは同時に起動すると後の方はエラーが表示されてすぐ終わります。
両方お手軽切り替え版
# -*- coding: utf-8 -*- import time import ctypes Kernel32 = ctypes.windll.Kernel32 # criticalsection def criticalsection(): for i in range(10): print(i) time.sleep(0.5) # Lock mutex = Kernel32.CreateMutexA(0, 1, "mutex-unique-name") result = Kernel32.WaitForSingleObject(mutex, -1) # ロック解除されるまで待つ #result = Kernel32.WaitForSingleObject(mutex, 0) # ロックされているかどうかだけ見る if result == 0x00000102: print("エラー:lock済") else: criticalsection() Kernel32.ReleaseMutex(mutex) Kernel32.CloseHandle(mutex)
コメントでどっちかお手軽に切り分けられるようにしました。
実際はclassなんかにして、フラグで切り替えできるようにするか、待ち時間を設定できる方が良いでしょう。