Листинг 9.11.Реализация interruptible_wait()
для std::condition_variable
с таймаутом
class interrupt_flag {
std::atomic flag;
std::condition_variable* thread_cond;
std::mutex set_clear_mutex;
public:
interrupt_flag(): thread_cond(0) {}
void set() {
flag.store(true, std::memory_order_relaxed);
std::lock_guard lk(set_clear_mutex);
if (thread_cond) {
thread_cond->notify_all();
}
}
bool is_set() const {
return flag.load(std::memory_order_relaxed);
}
void set_condition_variable(std::condition_variable& cv) {
std::lock_guard lk(set_clear_mutex);
thread_cond = &cv;
}
void clear_condition_variable() {
std::lock_guard lk(set_clear_mutex);
thread_cond = 0;
}
struct clear_cv_on_destruct {
~clear_cv_on_destruct() {
this_thread_interrupt_flag.clear_condition_variable();
}
};
};
void interruptible_wait(std::condition_variable& cv,
std::unique_lock& lk) {
interruption_point();
this_thread_interrupt_flag.set_condition_variable(cv);
interrupt_flag::clear_cv_on_destruct guard;
interruption_point();
cv.wait_for(lk, std::chrono::milliseconds(1));
interruption_point();
}
Если мы ждем какой-то предикат, то таймаут продолжительностью 1 мс можно полностью скрыть внутри цикла проверки предиката:
template
void interruptible_wait(std::condition_variable& cv,
std::unique_lock& lk,
Predicate pred) {
interruption_point();
this_thread_interrupt_flag.set_condition_variable(cv);
interrupt_flag::clear_cv_on_destruct guard;
while (!this_thread_interrupt_flag.is_set() && !pred()) {
cv.wait_for(lk, std::chrono::milliseconds(1));
}
interruption_point();
}
Правда, предикат при этом проверяется чаще, чем необходимо, но зато эту функцию легко использовать вместо простого вызова wait()
. Легко реализовать и другие варианты функций с таймаутом, например: ждать в течение указанного времени или 1 мс в зависимости от того, что меньше.
Ну хорошо, с ожиданием std::condition_variable
мы разобрались, а что сказать о std::condition_variable_any
? Всё точно так же или можно сделать лучше?
9.2.4. Прерывание ожидания std::condition_variable_any
Класс std::condition_variable_any
отличается от std::condition_variable
тем, что работает с любым типом блокировки, а не только с std::unique_lock
. Как выясняется, это сильно упрощает дело, так что мы сможем добиться более впечатляющих результатов, чем получилось с std::condition_variable
. Раз допустим любой тип блокировки, то можно написать и свой собственный класс, который захватывает (освобождает) как внутренний мьютекс set_clear_mutex
в классе interrupt_flag
, так и блокировку, переданную при вызове wait()
. Соответствующий код приведён в листинге ниже.
Листинг 9.12.Реализация interruptible_wait()
для std::condition_variable_any
class interrupt_flag {
std::atomic 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 lk(set_clear_mutex);
if (thread_cond) {
thread_cond->notify_all();
} else if (thread_cond_any) {
thread_cond_any->notify_all();
}
}
template
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
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
.
Читать дальше