Вы задаете несколько вопросов, но я сосредоточусь на ограничениях порядка, используемых в типичной реализации shared_ptr
, потому что я думаю, что это охватывает ключевую часть вашего вопроса.
Атомарная операция всегда атомарна по отношению к переменной (или POD), к которой она применяется; изменения одной переменной станут видимыми для всех потоков в согласованном порядке.
В вашем вопросе описан способ работы расслабленных атомарных операций:
std::memory_order_relaxed
по-прежнему является атомарным по отношению к той же переменной, но не предоставляет никаких других ограничений на загрузку и хранение по отношению к другим данным.
Ниже приведены 2 типичных сценария, в которых ограничения порядка для атомарной операции могут быть опущены (т. Е. С использованием std::memory_order_relaxed
):
Упорядочивание памяти не требуется, потому что нет зависимостей от других операций, или, как выразился комментатор, (..) не является частью инварианта, включающего другие области памяти.
Типичный пример - атомарный счетчик, увеличиваемый несколькими потоками, чтобы отслеживать, сколько раз произошло конкретное событие. Операцию приращения (fetch_add
) можно ослабить, если счетчик представляет значение, не зависящее от других операций.
Я считаю пример, приведенный cppreference, не очень убедительным, потому что счетчик ссылок shared_ptr
делает иметь зависимость; т.е. память удаляется, как только ее значение становится равным нулю. Лучшим примером является веб-сервер, отслеживающий количество входящих запросов только для целей отчетности.
Упорядочивание памяти необходимо, но нет необходимости в использовании ограничений упорядочения, потому что требуемая синхронизация уже прошла (IMO, это лучше объясняет, почему приращение счетчика ссылок shared_ptr
может быть ослаблено, см. Пример ниже).
Конструктор копирования / перемещения shared_ptr
может быть вызван только тогда, когда у него есть синхронизированное представление (ссылка на) скопированный / перемещенный экземпляр (или это было бы неопределенным поведением), и поэтому никакого дополнительного упорядочивания не требуется.
В следующем примере показано, как упорядочение памяти обычно используется реализацией shared_ptr
для изменения своего счетчика ссылок. Предположим, что все потоки выполняются параллельно после того, как sp_main
был освобожден (тогда счетчик ссылок shared_ptr
равен 10).
int main()
{
std::vector<std::thread> v;
auto sp_main = std::make_shared<int>(0);
for (int i = 1; i <= 10; ++i)
{
// sp_main is passed by value
v.push_back(thread{thread_func, sp_main, i});
}
sp_main.reset();
for (auto &t : v) t.join();
}
void thread_func(std::shared_ptr<int> sp, int n)
{
// 10 threads are created
if (n == 7)
{
// Only thread #7 modifies the integer
*sp = 42;
}
// The only thead with a synchronized view of the managed integer is #7
// All other threads cannot read/write access the integer without causing a race
// 'sp' going out of scope -> destructor called
}
Создание потока гарантирует (межпотоковую) связь «происходит раньше» между make_shared
(в main
) и конструктором копирования / перемещения sp
(внутри каждого потока). Следовательно, конструктор shared_ptr
имеет синхронизированное представление памяти и может безопасно увеличивать ref_count
без дополнительного упорядочивания:
ctrlblk->ref_count.fetch_add(1, std::memory_order_relaxed);
Что касается части уничтожения, поскольку только поток #7
записывает в общее целое число, остальным 9 потокам не разрешается обращаться к той же области памяти, не вызывая гонки. Это создает проблему, потому что все потоки разрушаются примерно в одно и то же время (предположим, что reset
в main
был вызван ранее), и только один поток собирается удалить общее целое число (тот, который уменьшает ref_count
с 1 до 0).
Крайне важно, чтобы последний поток имел синхронизированное представление памяти, прежде чем он удалит целое число, но поскольку 9 из 10 потоков не имеют синхронизированного представления, необходимо дополнительное упорядочение.
Деструктор может содержать что-то вроде:
if (ctrlblk->ref_count.fetch_sub(1, std::memory_order_acq_rel) == 1)
{
// delete managed memory
}
Атомарный ref_count
имеет один порядок модификации, и поэтому все атомарные модификации происходят в определенном порядке. Скажем, потоки (в этом примере), которые выполняют последние 3 декремента для ref_count
, - это поток #7
(3 → 2), #5
(2 → 1) и #3
(1 → 0). Оба декремента, выполняемые потоками #7
и #5
, происходят раньше в порядке модификации, чем тот, который выполняется #3
.
Последовательность выпуска становится следующей:
#7
(выпуск магазина) → #5
(чтение-изменение-запись, порядок не требуется) → #3
(получение загрузки)
Конечным результатом является то, что операция освобождения, выполняемая потоком #7
, синхронизировалась с операцией получения, выполняемой #3
, и целочисленная модификация (на #7
) гарантированно произошла до целочисленного уничтожения (на #3
).
Технически только потоки, которые получили доступ к управляемой области памяти, должны выполнять операцию освобождения, но поскольку разработчик библиотеки не знает о действиях потока, все потоки выполняют операцию освобождения при уничтожении.
Для окончательного уничтожения разделяемой памяти технически только последний поток должен выполнить операцию получения, и поэтому разработчик библиотеки shared_ptr
может оптимизировать, установив автономное ограждение, которое вызывается только последним потоком.
if (ctrlblk->ref_count.fetch_sub(1, std::memory_order_release) == 1)
{
std::atomic_thread_fence(std::memory_order_acquire);
// delete managed memory
}
person
LWimsey
schedule
08.01.2018
std::memory_order_relaxed
гарантирует атомарность, но не упорядочивает или синхронизирует. Таким образом, он работает для таких вещей, как счетчики статистики, но может быть сложно использовать, если счет является частью инварианта, включающего другие области памяти. - person Arch D. Robison   schedule 06.01.2018relaxed
, в то время как декремент используетacq/rel
, нетривиально, хотя - person LWimsey   schedule 06.01.2018delete
является зависимостью от полученного значения - person Curious   schedule 06.01.2018shared_ptr
порядок памяти. (Или, по крайней мере, попытку). - person LWimsey   schedule 06.01.2018delete
потребляет ... что? - person curiousguy   schedule 21.01.2019