read write lock ってものがあります。 pthread だと pthread_rwlock_t 。コレの私の思ってたセマンティクスは以下のようなもんです。
- writer lock を取ると普通の mutex lock みたいな感じ。その thread が unlock するまで、以降の reader lock と writer lock は block する。
- reader lock を取ると、それ以降の writer lock は block する。 reader lock は block せずに取れる
これは glibc の pthread のデフォルトの挙動なんですが、これは変えられるし、 POSIX によると環境によって違う挙動をすることもあるらしい、ってのを知りませんでした。
どういう時の挙動が変わるかというと、 reader lock が取られてて、 writer lock が待ってる時に次の reader lock をどう扱うか、っていう部分で、 writer lock が reader lock の終了を待ってる時は次の reader lock を block する、って挙動がありえるそうです。これはなんのためかというと、 reader がうじゃうじゃいて常に lock 取ってたりすると、永久に writer の順番が回ってこないという問題が回避できる、と。こういう問題を writer starvation というらしいです。
glibc の pthread では、 pthread_rwlockattr_setkind_np に PTHREAD_RWLOCK_PREFER_WRITER_NONRECURSIVE_NP を渡してやると、 writer starvation を回避する挙動になるとのこと。こっちの挙動の欠点は、名前に書いてある通り、 recursive な lock が deadlock に大変なりやすくなることで、
- thread1 が reader lock を取る
- thread2 が writer lock を取ろうとして待ちはじめる
- thread1 が reader lock を再び取ろうとするけど、 thread2 が待ってるから reader lock でも block されちゃう
って感じで簡単に deadlock しちゃいます。というか、今回このへん勉強してみたのはまさにこの deadlock をやらかしてしまったからでした。 reader lock は適当に同じスレッドでいくら取ってもいい、って理解だったので、共有物の getter 関数ぽいのは適当に全部 reader lock でくるんでおいて、 reader lock 取られてる中でも深く気にせずにその関数を読んでたところ、上述のシナリオになってしまった、という。
あと、 glibc の pthread は PTHREAD_RWLOCK_PREFER_WRITER_NP てのも用意してるけど、色んなとこの情報と glibc-2.7 のコード見る感じでは、名前に反してデフォルトの PTHREAD_RWLOCK_PREFER_READER_NP と同じ挙動のようでした。
まあ、たいていの場合は PTHREAD_RWLOCK_PREFER_WRITER_NONRECURSIVE_NP を使って、たまにあるアップデート時に writer lock するけど、そのアップデート自体はいつでもいいよ、みたいなものならデフォルトのを使う、みたいなのがいい感じなんでしょうか。
適当に参考にしたもの。前者はすごい詳しい。