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
.
9.2.5. Прерывание других блокирующих вызовов
Теперь с прерыванием ожидания условной переменной всё ясно, но как быть с другими блокирующими операциями ожидания: освобождения мьютекса, готовности будущего результата и т.д.? Вообще говоря, приходится прибегать к тому же трюку с таймаутом, который мы использовали для std::condition_variable
, потому что, если не влезать в код мьютекса или будущего результата, то нет никакого другого способа прервать ожидание, кроме как обеспечить выполнение ожидаемого условия. Но в отличие от условных переменных, мы точно знаем, чего ждем, поэтому можем организовать цикл внутри функции interruptible_wait()
.
Вот, например, как выглядит перегрузка interruptible_wait()
для std::future<>
:
template
void interruptible_wait(std::future
while (!this_thread_interrupt_flag.is_set()) {
if (uf.wait_for(lk, std::chrono::milliseconds(1) ==
std::future_status::ready)
break;
}
interruption_point();
}
Здесь мы ждем, пока либо будет установлен флаг прерывания, либо готов будущий результат, но блокирующее ожидание будущего результата продолжается в течение 1 мс на каждой итерации цикла. Это означает, что в среднем запрос на прерывание будет обнаружен с задержкой 0,5 мс, в предположении, что разрешение часов достаточно высокое. Функция wait_for
обычно ожидает в течение как минимум одного такта, поэтому если такт системных часов составляет 15 мс, то ждать придётся не одну, а 15 мс. Приемлемо это или нет, зависит от конкретных условий. Таймаут при необходимости можно уменьшить (если часы позволяют), но тогда поток будет чаще просыпаться для проверки флага, что увеличит накладные расходы на переключение задач.
На данный момент мы знаем, как можно обнаружить прерывание с помощью функций interruption_point()
и interruptible_wait()
, но что с этим потом делать?
9.2.6. Обработка прерываний
С точки зрения прерываемого потока, прерывание — это просто исключение типа thread_interrupted
, которое, следовательно, можно обработать, как любое другое исключение. В частности, его можно перехватить в стандартном блоке catch
:
try {
do_something();
} catch (thread_interrupted&) {
handle_interruption();
}
Таким образом, прерывание можно перехватить, каким-то образом обработать и потом спокойно продолжить работу. Если мы так поступим, а потом другой поток еще раз вызовет interrupt()
, то поток будет снова прерван, когда в очередной раз вызовет функцию interruption_point()
. Возможно, это и неплохо, если вы выполняете последовательность независимых задач; текущая задача будет прервана, а поток благополучно перейдёт к следующей задаче в списке.