Возможно, вы заметили шаблонный код в предыдущем примере: оба цикла ожидания имеют следующую структуру:
lock (locker) { while (!flag) Monitor.Wait(locker); flag = false; ... } |
где flag устанавливается в true в другом потоке. В действительности это имитация AutoResetEvent. А если опустить flag=false, то ManualResetEvent. Используя целочисленное поле, Pulse и Wait, можно имитировать Semaphore. Фактически единственный WaitHandle, который нельзя имитировать при помощи Pulse и Wait – это Mutex, так как эта функциональность предоставляется оператором lock.
Имитация статических методов, которые работают с несколькими WaitHandle, в большинстве случаев проста. Эквивалент вызова WaitAll с несколькими WaitHandle – не что иное, как условие блокировки, включающее все флаги, используемые вместо WaitHandle:
lock (locker) { while (!flag1 &&!flag2 &&!flag3...) Monitor.Wait(locker); |
Это может быть особенно полезно, так как WaitAll в большинстве случаев не пригоден к использованию из-за проблем с унаследованным COM-кодом. Имитация WaitAny – просто вопрос замены оператора && оператором ||.
С имитацией SignalAndWait сложнее. При вызове он сигналит на одном хендле при ожидании на другом в атомарной операции. Ситуация аналогична транзакциям в распределенной базе данных – необходим двухфазный commit! Если, например, необходимо посигналить флагом flagA при ожидании на флаге flagB, придется разделить каждый флаг на два, получив в результате код типа такого:
|
|
lock (locker) { flagAphase1 = true; Monitor.Pulse(locker); while (!flagBphase1) Monitor.Wait(locker); flagAphase2 = true; Monitor.Pulse(locker); while (!flagBphase2) Monitor.Wait(locker); } |
возможно, с дополнительной rollback-логикой для отката flagAphase1, если первый Wait сгенерирует исключение в результате прерывания по Interrupt или аварийного завершения потока по Abort. В этой ситуации использовать WaitHandle гораздо проще. В действительности, однако, атомарная сигнализация-и-ожидание – это редкое требование.