9.2.7. Прерывание фоновых потоков при выходе из приложения

Представьте себе приложение для поиска в файловой системе настольного ПК. Оно должно не только взаимодействовать с пользователем, но и следить за состоянием файловой системы, обнаруживать изменения и обновлять свой индекс. Обычно такие операции поручаются фоновому потоку, чтобы пользовательский интерфейс мог реагировать на действия пользователя. Фоновый поток должен работать на протяжении всего времени жизни приложения; он запускается на этапе инициализации и трудится, пока приложение не завершится. Обычно это происходит при останове операционной системы, так как приложение должно постоянно поддерживать индекс в актуальном состоянии. Как бы то ни было, когда приложение завершается, надо аккуратно остановить и фоновые потоки, например, прервав их.

В следующем листинге показана возможная реализация управления потоками в такой программе.

Листинг 9.13. Фоновый мониторинг файловой системы

std::mutex config_mutex;

std::vector<interruptible_thread> background_threads;

void background_thread(int disk_id) {

 while (true) {

  interruption_point(); ← (1)

  fs_change fsc = get_fs_changes(disk_id); ← (2)

  if (fsc.has_changes()) {

   update_index(fsc); ← (3)

  }

 }

}

void start_background_processing() {

 background_threads.push_back(

  interruptible_thread(background_thread, disk_1));

 background_threads.push_back(

  interruptible_thread(background_thread, disk_2));

}

int main() {

 start_background_processing(); ← (4)

 process_gui_until_exit(); ← (5)

 std::unique_lock<std::mutex> lk(config_mutex);

 for (unsigned i = 0; i < background_threads.size(); ++i) {

  background_threads[i].interrupt(); ← (6)

 }

 for (unsigned i = 0; i < background_threads.size(); ++i) {

  background_threads[i].join(); ← (7)

 }

}

В самом начале запускаются фоновые потоки (4). Затем главный поток продолжает обслуживать пользовательский интерфейс (5). Когда пользователь хочет выйти из приложения, фоновые потоки прерываются (6), после чего главный поток ждет их завершения (7), и только потом выходит сам. Каждый фоновый поток исполняет цикл, в котором следит за изменениями на диске (2) и обновляет индекс (3). На каждой итерации цикла поток проверяет, не прервали ли его, вызывая функцию interruption_point() (1).

Почему мы прерываем все потоки до того, как начинать ждать их завершения? Почем нельзя прервать один поток, дождаться его, потом прервать следующий и так далее? Все из-за параллелизма. Поток не завершается сразу после прерывания, так как должен добраться до очередной точки прерывания, а затем, перед выходом, выполнить все деструкторы и код обработки исключений. Если главный поток будет присоединять прерванные потоки сразу после прерывания, то ему придётся ждать, хотя в это время он мог бы делать полезную работу — прерывать другие потоки. Поэтому мы поступаем по-другому — начинаем ждать только тогда, когда больше никакой работы не осталось (все потоки уже прерваны). Заодно это позволяет прерываемым потокам обрабатывать прерывания параллельно, так что общее время завершения, возможно, уменьшится.

Описанный механизм прерывания легко развить, добавив дополнительные прерываемые вызовы или запретив прерывания на определенном участке кода, но это я оставляю читателю в качестве упражнения.