Pythonの排他制御 (flock) で注意すること

2010年2月14日

関連記事
今回はファイルロックを使用する際の注意点を挙げてみます。
まずはサンプルソースです。
getlock_ex.py

排他ロックを獲得するプログラムです。
排他ロック獲得後5秒sleepして起き上がり、ロックを解放して終了します。 

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
#!/usr/bin/python 
# coding: utf-8 
import fcntl, time 
def main(): 
    lockfp = file("res.lock", "r") 
    fcntl.flock(lockfp.fileno(), fcntl.LOCK_EX) 
    print "get lock_ex (fileno: %d)!" % lockfp.fileno() 
    print "go sleep 5 (sec)..." 
    time.sleep(5) 
    print "awake!!!" 
    fcntl.flock(lockfp.fileno(), fcntl.LOCK_UN) 
    print "unlock lock_ex (fileno: %d)!" % lockfp.fileno() 
    lockfp.close() 

if ( __name__ == "__main__" ): 
    main()
getlock_sh.py

共有ロックを獲得するプログラムです。
共有ロック獲得後5秒sleepして起き上がり、ロックを解放して終了します。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
#!/usr/bin/python 
# coding: utf-8 import fcntl, time 

def main():
    lockfp = file("res.lock", "r")
    fcntl.flock(lockfp.fileno(), fcntl.LOCK_SH)
    print "get lock_sh (fileno: %d)!" % lockfp.fileno()
    print "go sleep 5 (sec)..."
    time.sleep(5)
    print "awake!!!"
    fcntl.flock(lockfp.fileno(), fcntl.LOCK_UN)
    print "unlock lock_sh (fileno: %d)!" % lockfp.fileno()
    lockfp.close()

if ( __name__ == "__main__" ): 
    main()

上記のプログラム2つコンソールを開いて、同時に実行すると分かるのですが、 まずfileno()が返す整数値が異なっていても、同じファイルロック資源をキチンと 見に行って、ロックする・しないを判断してくれているということです。


Unix系の場合、ファイルを一意に識別するための情報としてinode番号をいうIDを付与しているのですが、 上記プログラムでfileno()の値を見る限りこれとfileno()は異なるようです。
プロセスごとに異なるので恐らくファイルディスクリプタに関係する番号なのでしょう。
ファイルディスクリプタは、プロセスごとに付与される情報で、そこにどのファイルをオープンしているのかや、 そのファイルのstat情報などを保存しています。

でも、以下のようにgetlock_sh.pyを書き換えると、不思議なことが起きます。
getlock_sh.py自信が解放したはずのロックが解放できておらず、2回目のロックが実行できないのです。
従って、getlock_sh.pyは永眠してしまいます。。。


getlock_sh2.py

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
#!/usr/bin/python 
# coding: utf-8 import fcntl, time 

def main():
    lockfp2 = file("res.lock", "r")
    lockfp = file("res.lock", "r")
    fcntl.flock(lockfp.fileno(), fcntl.LOCK_SH)
    print "get lock_sh (fileno: %d)!" % lockfp.fileno()

    fcntl.flock(lockfp2.fileno(), fcntl.LOCK_UN)               # ★

    print "unlock_sh (fileno: %d)!" % lockfp.fileno()
    fcntl.flock(lockfp2.fileno(), fcntl.LOCK_EX)
    print "get lock_ex (fileno: %d)!" % lockfp2.fileno()
    print "go sleep 5 (sec)..."
    time.sleep(5) print "awake!!!"

    fcntl.flock(lockfp2.fileno(), fcntl.LOCK_UN)               # ★

    fcntl.flock(lockfp2.fileno(), fcntl.LOCK_SH)

    fcntl.flock(lockfp.fileno(), fcntl.LOCK_UN)                 # ★

    lockfp.close() 

if ( __name__ == "__main__" ):
    main()


上記★部分をご覧下さい。


このプログラムではlockfpを用いて共有ロックを獲得し、その後lockfp2にて当該ロックを解放しようとしています。 同じファイルである「res.lock」のロックを獲得しているはずが、ファイルディスクリプタが異なるため、 実際にはLOCK_UNでロックを解放できていないのです。


従って次の排他ロック(LOCK_EX)が共有ロックを獲得しているままなので、永遠に獲得できず、 sleepしたまま起き上がってこれないのです。

よって正しくは、上記赤い行のロック取得・解放をlockfpに対して行えばいいのです。