В многопоточной POSIX-программе показан фрагмент: int counter = 0; void* inc(void*){ for(int i=0;i

10 Окт в 13:31
5 +1
0
Ответы
1

Коротко — потому что ++ не атомарен. Операция counter++ разлагается на загрузку значения из памяти, увеличение и запись обратно. Два потока могут одновременно загрузить одно и то же значение, оба увеличить и записать — одно из приращений «теряется». Это классическая гонка данных dataracedata racedatarace.

Ниже — несколько способов исправления с кратким обоснованием и влиянием на производительность.

1) Мьютекс pthreadmutextpthread_mutex_tpthreadm utext

Как исправляет: критическая секция вокруг counter++ гарантирует взаимное исключение, только один поток выполняет ++ одновременно.Пример:
include

int counter = 0;
pthread_mutex_t m = PTHREAD_MUTEX_INITIALIZER;
void incvoid</em>void</em>void</em>{
for(int i=0;i<1000000;i++){
pthread_mutex_lock(&m);
counter++;
pthread_mutex_unlock(&m);
}
return NULL;
}

Производительность: простая и надёжная, но при высокой частоте коротких критических секций даёт серьёзную деградацию. Издержки: захват/освобождение мьютекса лок/уловкалок/уловкалок/уловка, возможные контекстные переключения при ожидании, сильное касание кеш-линий cache−linebouncingcache-line bouncingcachelinebouncing — фактически сериализация всех приращений.

2) Атомарные операции C11stdatomicилиGCC/ClangintrinsicsC11 stdatomic или GCC/Clang intrinsicsC11stdatomicилиGCC/Clangintrinsics

Как исправляет: атомарный инкремент выполняется как одна атомарная инструкция илиспомощьюCASили с помощью CASилиспомощьюCAS, предотвращая потерю инкрементов.Пример C11C11C11:
include

atomic_int counter = ATOMIC_VAR_INIT000;
void incvoid</em>void</em>void</em>{
for(int i=0;i<1000000;i++)
atomic_fetch_add_explicit(&counter, 1, memory_order_relaxed);
return NULL;
}

Можно использовать GCC: atomic_fetch_add(&counter, 1, ATOMIC_RELAXED) или старые __sync_fetch_and_add.Производительность: быстрее, чем мьютекс в большинстве случаев, особенно если использован memory_order_relaxed нетлишнихбарьеровнет лишних барьеровнетлишнихбарьеров. Но при сильной конкуренции атомик всё равно вызывает кеш-бег cachecoherencytrafficcache coherency trafficcachecoherencytraffic и становится узким местом — инкременты логически сериализуются на уровне кэш-контроля.

3) Батчинг / per-thread локальные счётчики рекомендуемыйподходпривысокомнасыщениирекомендуемый подход при высоком насыщениирекомендуемыйподходпривысокомнасыщении

Идея: каждый поток инкрементирует локальную переменную без синхронизации, и только периодически или в конце одним атомиком добавляет накопленное значение в глобальный counter. Это сильно уменьшает количество синхронизаций.Пример:
int counter = 0; // глобальный
void incvoid</em>void</em>void</em>{
int local = 0;
for(int i=0;i<1000000;i++){
local++;
if local==1000local == 1000local==1000{ // flush каждые 1000
atomic_fetch_add(&counter, local, ATOMIC_RELAXED);
local = 0;
}
}
if locallocallocal atomic_fetch_add(&counter, local, ATOMIC_RELAXED);
return NULL;
}Производительность: очень хорошая при больших объёмах инкрементов — минимизирует трафик кеш-линии и уменьшает количество атомик-операций.

4) Шардирование/striped counters

Идея: иметь массив счетчиков по числу потоков илипоядрамили по ядрамилипоядрам: каждый поток пишет только в свой слот, а финальная сумма вычисляется суммированием всех слотов.Производительность: как per-thread локальные счётчики, но более систематично предотвращает конфликт записи в одну кэш-линию надовыравниватьслоты,чтобыизбежатьfalsesharingнадо выравнивать слоты, чтобы избежать false sharingнадовыравниватьслоты,чтобыизбежатьfalsesharing.

5) Детерминированное планирование / сериализация

Что это значит: сделать исполнение потоков детерминированным/последовательным например,запускатьодинпотокдоконца,затемдругой;илииспользоватьruntime,которыйобеспечиваетдетерминированныеинтерлевингинапример, запускать один поток до конца, затем другой; или использовать runtime, который обеспечивает детерминированные интерлевингинапример,запускатьодинпотокдоконца,затемдругой;илииспользоватьruntime,которыйобеспечиваетдетерминированныеинтерлевинги.Как исправляет: устраняет гонки за счёт отсутствия одновременного доступа, даёт строго детерминированный counter.Производительность: обычно значительно хуже, т. к. лишает программу параллелизма — если цель была ускорение за счёт многопоточности, этот подход сводит его на нет. Может быть полезен для отладки или воспроизводимого тестирования record−replay,Dthreadsит.п.record-replay, Dthreads и т.п.recordreplay,Dthreadsит.п..

Дополнения по выбору памяти/порядка memoryorderingmemory orderingmemoryordering

Если глобальный счётчик используется только для учёта суммарных инкрементов и вам не требуется упорядоченность относительно других операций — atomic_fetch_add с memory_order_relaxed достаточно и быстрее недаётлишнихбарьеровне даёт лишних барьеровнедаётлишнихбарьеров.Если нужен строгий глобальный порядок, используйте memory_order_seq_cst поумолчаниюпо умолчаниюпоумолчанию или stronger ordering, но это может быть дороже.

Коротко о производительности и выборе

Для корректности: достаточно атомарной операции или мьютекса.Для простоты и совместного доступа с другими данными: мьютекс — самый прямой, но может стать узким местом.Для высокой производительности при большом количестве инкрементов: per-thread буферизация или sharded counters + редукция суммированиевконцесуммирование в концесуммированиевконце — наилучший компромисс.Атомики — удобны и обычно быстрее мьютекса, но при очень высокой конкуренции всё равно будут замедляться из‑за кеш‑кооперации.Детерминизация решает проблему согласованности только ценой потери параллелизма; в реальных рабочих нагрузках это редко оптимально, разве что для отладки/воспроизводимости.

Резюме: причина — гонка данных. Простое исправление — использовать atomic_fetch_add илимьютексили мьютексилимьютекс. Для максимальной производительности при интенсивных инкрементах — агрегировать в локальные счётчики или использовать полосные shardedshardedsharded счётчики, чтобы уменьшить конкуренцию за одну память/кэш-линию.

10 Окт в 13:53
Не можешь разобраться в этой теме?
Обратись за помощью к экспертам
Гарантированные бесплатные доработки в течение 1 года
Быстрое выполнение от 2 часов
Проверка работы на плагиат
Поможем написать учебную работу
Прямой эфир