Достаточно сказано, что вам следует подумать дважды, а затем трижды, прежде чем отсоединять нить. Однако рассуждение часто бывает чем-то абстрактным: сложно отслеживать и взаимодействовать с отдельным потоком, он может пережить основной поток и тому подобное. Статика тоже плохая, сами понимаете. Но вместе они составляют действительно устрашающее комбо. Вот настоящая ужасная история о том, что идет не так, когда оторвавшаяся нить касается вашей статики.

Короткий рассказ

  1. Создается статический объект HappySingleton. Это может быть любая статическая продолжительность хранения, например глобальная в пространстве имен или статическая в источнике или функции.
  2. Тема SinisterThread запускается и отключается.
  3. Основной поток завершается после возврата main() или вызова exit(). Это приводит к разрушению каждого статического объекта. В том числе и наш бедныйHappySingleton.
  4. Еще жив SinisterThread читает HappySingleton. Читает освобожденную память.
  5. Бум.

Уродливые детали

Вы прочитали яркую версию рассказа. Разве это не чудесно, когда ваше приложение вылетает из-за ошибки? Желаю, чтобы все ошибки были такими. Но на этот раз у нас есть кое-что особенное.

В действительности чтение освобожденной памяти относительно редко приводит к сбоям. Намного реже, чем хотелось бы. Среда выполнения может помочь здесь в некоторых умных вещах. Или хотя бы сообщить ОС, что память освобождена. Но им и не нужно. Стандарт не принуждает их, поскольку впоследствии намного проще повторно использовать память, чем повторно запрашивать ее у ОС.

А доступ к статике выглядит очень безопасным для компиляторов. Здесь нет умного персонала.

В конце концов, вы можете прожить годы в продакшене под большой нагрузкой, не зная, что у вас есть какие-то проблемы (здесь только мои предположения!). Что в конце концов делает проблему не такой уж зловещей. В конце концов, у вас есть проблемы только в конце работы вашего приложения, так что кого это волнует.

Загадочная история с самого начала

Учитывая приложение, скомпилированное для Windows с MSVC и для Linux с CLang. Иногда по непонятным причинам возвращается ненулевое значение. В журналах версии Linux иногда может встречаться строка

terminating with uncaught exception of type std::__1::system_error: mutex lock failed: Invalid argument

Лучше чем ничего!

При просмотре исходных текстов CLang libcxx и после этого через документацию ОС получено сообщение. Invalid argument означает EINVALUE ошибку для pthread_mutex_lock() вызова. Это, вероятно, означает, что мьютекса нет.

Мьютекс. Из десятков.

Следующим шагом является тестовый запуск с ведением журнала отладки при каждом создании, уничтожении и блокировке мьютексов. Тут нужен мощный отладчик, MSVC использовался как более удобный автору (а значит и запуск производился на Windows). На запуск потребовалось время, и было получено несколько мегабайт журнала.

Выполнив некоторую обработку журнала, для каждого мьютекса были получены такие фрагменты (в формате <count> [<operation>] <thread> <mutex_address>):

MUTEX 0x08e57a70
 1 [create_mutex] Main Thread 31080 0x08e57a70
 7 [lock_mutex] Worker3 32548 0x08e57a70
 2 [lock_mutex] AsyncExecutor 25272 0x08e57a70
 1 [destroy_mutex] Main Thread 31080 0x08e57a70

И один мьютекс был другим. После разрушения у него было несколько lock() вызовов. И, кстати, Windows, похоже, не возражала против этого.

Трассировка стека создания показала, что мьютекс был членом статического объекта. Сначала это было шоком, но теперь вы знаете ответ.

Выводы

  1. Отдельные потоки - это плохо. Еще одно яблоко в эту корзину.
  2. Статика плохая. Здесь тоже.
  3. Вместе они даже интереснее. Подумайте о ситуации, когда отдельные потоки предназначены для выполнения существенной работы приложения даже после того, как основной может вернуться. Я собирался сказать: не думай!
  4. Разрушенная статика может дать загадку. И для вашей среды выполнения тоже.
  5. Linux получает оценку за то, что он немного быстрее терпит неудачу. Хотя не так уж и много.