std::thread

Если поток не завершил работу, не вызваны методы join() или detach(), но его деструктор thread уже запущен, то программа аварийно завершится вызовом std::terminate.

После успешного вызова на потоке методов join() или detach() метод joinable() будет возвращать ложь.

Вызов метода join() на одном и том же объекте thread из разных потоков - это undefined behavior, в том числе потому что нельзя делать join() потоку, который возвращает joinable() == false. Это приводит к генерации исключения.

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

Что случится с detached-потоками, когда программа выйдет из main?

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

std::conditional_variable

Важно знать, что conditional_variable иногда может просыпаться и без вызова .notify_one(), поэтому более безопасный код будет выглядеть так:

bool signaled = false;

// start background threads...
// someone will set signaled as true, then call cv.notify_one()

{
    std::unique_lock<std::mutex> lock(mutex);
    while (!signaled) {
        cv.wait(lock);
    }
    signaled = false;
}

False-sharing

Существует два типа разделения кэш-линий: true sharing и false sharing.

True sharing - это когда потоки имеют доступ к одному и тому же объекту памяти, например, общей переменной или примитиву синхронизации.

False sharing - это доступ к разным данным, но по каким-то причинам оказавшимся в одной кэш-линии процессора.

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

В случае постоянной модификации данных в условиях false sharing, процессору в соответствии с протоколом когерентности кэша необходимо инвалидировать эту кэш-линию целиком для остальных ядер процессора.

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

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

  1. Выравнивание массива
  2. Наличие подкладки до 64 байт (padding)

Заимствования:

c++ - When should I use std::thread::detach? - Stack Overflow

Делиться не всегда полезно: оптимизируем работу с кэш-памятью / Хабр (habr.com)