9.2.4. Прерывание ожидания std::condition_variable_any

Класс std::condition_variable_any отличается от std::condition_variable тем, что работает с любым типом блокировки, а не только с std::unique_lock<std::mutex>. Как выясняется, это сильно упрощает дело, так что мы сможем добиться более впечатляющих результатов, чем получилось с std::condition_variable. Раз допустим любой тип блокировки, то можно написать и свой собственный класс, который захватывает (освобождает) как внутренний мьютекс set_clear_mutex в классе interrupt_flag, так и блокировку, переданную при вызове wait(). Соответствующий код приведён в листинге ниже.

Листинг 9.12. Реализация interruptible_wait() для std::condition_variable_any

class interrupt_flag {

 std::atomic<bool> flag;

 std::condition_variable* thread_cond;

 std::condition_variable_any* thread_cond_any;

 std::mutex set_clear_mutex;

public:

 interrupt_flag():

  thread_cond(0), thread_cond_any(0) {}

 void set() {

  flag.store(true, std::memory_order_relaxed);

  std::lock_guard<std::mutex> lk(set_clear_mutex);

  if (thread_cond) {

   thread_cond->notify_all();

  } else if (thread_cond_any) {

   thread_cond_any->notify_all();

  }

 }

 template<typename Lockable>

 void wait(std::condition_variable_any& cv, Lockable& lk) {

  struct custom_lock {

   interrupt_flag* self;

   Lockable& lk;

   custom_lock(interrupt_flag* self_,

    std::condition_variable_any& cond,

    Lockable& lk_): self(self_), lk(lk_) {

    self->set_clear_mutex.lock();  ← (1)

    self->thread_cond_any = &cond; ← (2)

   }

   void unlock() { ← (3)

    lk.unlock();

    self->set_clear_mutex.unlock();

   }

   void lock() {

    std::lock(self->set_clear_mutex, lk); ← (4)

   }

   ~custom_lock() {

    self->thread_cond_any = 0; ← (5)

    self->set_clear_mutex.unlock();

   }

  };

  custom_lock cl(this, cv, lk);

  interruption_point();

  cv.wait(cl);

  interruption_point();

 }

 // остальное, как и раньше

};

template<typename Lockable>

void interruptible_wait(std::condition_variable_any& cv,

 Lockable& lk) {

 this_thread_interrupt_flag.wait(cv, lk);

}

Наш класс блокировки должен захватить внутренний мьютекс set_clear_mutex на этапе конструирования (1) и затем записать в переменную thread_cond_any указатель на объект std::condition_variable_any, переданный конструктору (2). Ссылка на объект Lockable сохраняется для последующего использования; он должен быть уже заблокирован. Теперь проверять, был ли поток прерван, можно, не опасаясь гонки. Если в этой точке флаг прерывания установлен, то это было сделано до захвата мьютекса set_clear_mutex. Когда условная переменная вызывает нашу функцию unlock() внутри wait(), мы разблокируем объект Lockable и внутренний мьютекс set_clear_mutex (3). Это позволяет потокам, которые пытаются нас прервать, захватить set_clear_mutex и проверить указатель thread_cond_any, когда мы уже находимся в wait(), но не раньше. Это именно то, чего мы хотели (но не смогли) добиться в случае std::condition_variable. После того как wait() завершит ожидание (из-за получения сигнала или вследствие ложного пробуждения), она вызовет нашу функцию lock(), которая снова захватит внутренний мьютекс set_clear_mutex и заблокирует объект Lockable (4). Теперь можно еще раз проверить, не было ли прерываний, пока мы находились в wait(), и только потом обнулить указатель thread_cond_any в деструкторе custom_lock (5), где также освобождается set_clear_mutex.