ファイヤープロジェクト
より高度な同期
2007-10-27T14:00+09:00   matsu
ロックで基本的な同期は実現できるが,限界がある.より高度な表現でより高度な同期を実現するための関数があるようなので,試してみた.
ロックは,「ただ資源を待つ」という単純な同期のみを実現する(※). なので,例えば
「xがyを越える」という条件が満たされるのを待つ
などという高度な待ち方ができない. やるとすれば,
待って,「xがyを越える」という条件が満たされていなければ,また「待つ」
ということである. コードにするとこんな感じだろうか.
実行すると,以下のようになる.
$ ./problem_sample 
condCheck x = 0 / y = 12
condChange x = 1 / y = 11
condChange x = 2 / y = 10
condCheck x = 2 / y = 10
condChange x = 3 / y = 9
condChange x = 4 / y = 8
condCheck x = 4 / y = 8
condChange x = 5 / y = 7
condChange x = 6 / y = 6
condCheck x = 6 / y = 6
condChange x = 7 / y = 5
condChange x = 8 / y = 4
condCheck x = 8 / y = 4
### condCheck x > y !!
condChange x = 9 / y = 3
condChange x = 10 / y = 2
condChangeを実行するスレッドによってx,yの値が更新される. そしてcondCheckによって,xがyの値を越えたかをチェックしている. 今回は問題を明確にするために意図的に値を調整したわけであるが,
condChange x = 7 / y = 5
の段階では検出できずに,
condChange x = 8 / y = 4
のときにようやく検出できている. これは,condChangeからcondCheckへは何の通知もされず,condCheckはただポーリングをして条件をチェックしているからである. ポーリング時,無限ループをすると資源を浪費するのでsleepなどをかますことんなるが,タイムリな検出を行うためには,sleep時間をチューニングする必要がある. より複雑な問題では,このチューニングも困難になったりする. ロックとポーリングをベースとした単純な同期ではこのような限界がある.
※ 資源の獲得と開放(ロックとアンロック)によって,「待つ」という行為を実現する.
上の
条件を満たしたことをタイムリに検出できない
という問題は,ロックの方法を工夫してある程度解決することができる. 方針は,
条件を変える側は条件を満たすまでロックをしたままとし,
条件を検出擦る側は,ロック時のブロックによって同期する
コードにするとこんな感じだろうか.
実行すると,以下のようになる.
$ ./problem_sample2 
condCheck x = 0 / y = 12
condCheck x = 0 / y = 12
condCheck x = 0 / y = 12
condCheck x = 0 / y = 12
condCheck x = 0 / y = 12
condChange x = 1 / y = 11
condChange x = 2 / y = 10
condChange x = 3 / y = 9
condChange x = 4 / y = 8
condChange x = 5 / y = 7
condChange x = 6 / y = 6
condChange x = 7 / y = 5
condCheck x = 7 / y = 5
### condCheck x > y !!
condChange x = 8 / y = 4
condChange x = 9 / y = 3
condChange x = 10 / y = 2
condChangeでは,まずロックし,条件を変更していく. 変更後,条件が満たされればロックを開放する. condCheckでは,ロックをし,条件が満たされているかをチェックする. 満たされていなければやはりsleepしてロックと条件チェックを繰り返すのだが,condChangeの条件の変更が開始されればロック関数でブロックされる. そしてcondChangeで条件が満たされアンロックされた時点でcondCheckのロック関数が返り,即座に検出されているように見える. この方法にもやはり問題がある. condChange側の条件の変更が開始されるまでは,やはりcondCheck側ではポーリングを行うことになり,改善はされているもののsleepの調整という問題は残ったままである. また,ロジッックも多少複雑であり,条件の変更とチェックを行う役者が増えれば,すぐにわけがわからなくなる(※).
※ マルチスレッドの同期のデバッグは厄介である.
前置きが長くなった. 上記2つのサンプルのネックは,結局条件をポーリングによってチェックしている点である. 条件を変更したスレッドは,条件が満たされたかどうかすぐにわかるのだから(変更したあとチェックすればよい),
条件が満たされていれば,待っている人に通知する
という表現ができれば,タイムリな検出も実現できる. コードもシンプルでわかりやすいし,sleepのチューニングといったうっとうしい問題も必要ない. pthreadでは,「条件変数操作」といったもので,スレッド間の通知を行うことができる. より具体的には,
条件変数を指定して待つ
という関数と,
条件変数を指定して通知する
という関数がある(※). アプリケーションの特定の条件は,条件変数によって表現/管理する. プログラマは特定の条件を待つ際には,その条件を表現することにした条件変数を指定して待つ関数
int pthread_cond_wait(pthread_cond_t *cond,
                      pthread_mutex_t *mutex);
int pthread_cond_timedwait(pthread_cond_t *cond,
                           pthread_mutex_t *mutex,
                           const struct timespec *abstime);
を記述する. 前者は待ち続け,後者はabstimeでタイムアウトする. そして,条件を変更する処理を記述した後,その特定の条件をチェックし,満たされていれば,上の条件変数を指定して,通知関数
int pthread_cond_signal(pthread_cond_t *cond);
int pthread_cond_broadcast(pthread_cond_t *cond);
を記述する. 前者は待っているスレッドのうち一つを任意に選んで通知し,後者は全待ちスレッドに通知する. 通知されたスレッドは再開する(pthread_cond_waitやpthread_cond_timewaitからぬける).
※ javaでいうところのwaitとnotify,notifyall等である. ただ,pthreadの方が,より多彩/柔軟な扱いができるようである.
さっそく条件変数操作を行ってみたいが,これには注意が必要である. 待ちと通知の行き違いを防ぐことである. 同期のための条件変数操作であるが,これ自体,同期をとる必要がある. 単純に考えると,通常のシーケンスは下のようになる.
だが,上のシーケンスだと,同期処理がないので,タイミングによってはwait側の条件チェックから条件待ちまでの間に,通知側の条件変更と通知が行われてしまうことがありうる.
こうなると,実際には条件が満たされているのに,wait側ではそれを通知されずに待ちぼうけするととなる. これを防ぐために,pthread_cond_waitとpthread_cond_timewaitでは,同期処理を行っている. この同期処理のためにプログラマは,条件変数は常にmutexと結びつけて管理する必要がある. 具体的には以下のようなシーケンスとなる.
上のシーケンスでもwait関数の最初のアンロックと条件待ち処理の間で上と同様の同期問題が発生しそうだが,それはPthreadの使用としてアトミックに実行されることが保証されている. まとめると,pthread_cond_waitやpthread_cond_timewaitを使用するコーディングでは,大体以下の流れとなる.
  1. mutexロック
  2. 条件チェック
  3. pthread_cond_waitやpthread_cond_timewaitの呼び出し. この時,上でロックしたmutexも渡す.
  4. mutexアンロック
そして通知側のコーディングでは,大体以下の流れとなる.
  1. mutexロック 当然同期するwait側と同じmutexをロックする.
  2. 値変更
  3. 条件チェック
  4. pthread_cond_signalやpthread_cond_broadcastの呼び出し. 当然同期するwait側と同じ条件変数を使用する.
  5. mutexアンロック
ようやくサンプル. 最初の
「xがyを越える」
というのを検知するサンプルを作成してみる.
サンプルでは,condCheckでポーリングのループをする必要がなくなっている. また,ロックを工夫しても残っていた値の変更開始までのループの問題もなくなっているのを確認するため,condChangeの最初にsleepをいれている. 実行すると,以下のようになる.
$ ./cond_sample 
condCheck x = 0 / y = 12
condChange x = 1 / y = 11
condChange x = 2 / y = 10
condChange x = 3 / y = 9
condChange x = 4 / y = 8
condChange x = 5 / y = 7
condChange x = 6 / y = 6
condChange x = 7 / y = 5
### condCheck x > y !!
condChange x = 8 / y = 4
condChange x = 9 / y = 3
condChange x = 10 / y = 2
condCheckにて変更が即座に検出されていることがわかる.
matsu(C)
Since 2002
Mail to matsu