Я слышал, что я ++ не является потокобезопасным, is ++ я потокобезопасным?

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

Однако мне интересно о ++ i. Насколько я могу судить, это сводилось бы к одной команде сборки, такой как "добавить r1, r1, 1", и поскольку это только одна команда, она была бы бесперебойной с помощью контекстного переключателя.

Может ли кто-нибудь уточнить? Я предполагаю, что используется платформа x86.

Ответ 1

Ты слышал не так. Вполне возможно, что "i++" является потокобезопасным для конкретного компилятора и конкретной архитектуры процессора, но он не предусмотрен в стандартах вообще. На самом деле, поскольку многопоточность не является частью стандартов ISO C или С++ (a) вы не можете считать что-либо потокобезопасным на основе того, что, по вашему мнению, будет скомпилировано до.

Весьма возможно, что ++i может скомпилировать произвольную последовательность, такую ​​как:

load r0,[i]  ; load memory into reg 0
incr r0      ; increment reg 0
stor [i],r0  ; store reg 0 back to memory

который не был бы потокобезопасным на моем (мнимом) процессоре, который не имеет инструкций по наращиванию памяти. Или это может быть умным и скомпилировать его в:

lock         ; disable task switching (interrupts)
load r0,[i]  ; load memory into reg 0
incr r0      ; increment reg 0
stor [i],r0  ; store reg 0 back to memory
unlock       ; enable task switching (interrupts)

где lock отключается, а unlock - прерывания. Но даже тогда это может быть потокобезопасным в архитектуре с более чем одним из этих разделяющих память процессоров (lock может отключать прерывания только для одного процессора).

Сам язык (или библиотеки для него, если он не встроен в язык) предоставит потокобезопасные конструкции, и вы должны использовать их, а не полагаться на свое понимание (или, возможно, непонимание) того, какой машинный код будет сгенерирован.

Такие вещи, как Java synchronized и pthread_mutex_lock() (доступные для C/С++ в некоторых операционных системах), - это то, что вам нужно посмотреть в (a).


(a) Этот вопрос задавали до того, как были завершены стандарты C11 и С++ 11. Эти итерации теперь вносили поддержку потоковой передачи в спецификации языка, включая атомные типы данных (хотя они и потоки в целом необязательны, по крайней мере, на C).

Ответ 2

Вы не можете сделать выражение о сложениях ни о ++ i, ни о я ++. Зачем? Рассмотрим увеличение 64-разрядного целого числа в 32-битной системе. Если базовая машина не имеет четырехзначное слово "load, increment, store", то приращение этого значения потребует нескольких инструкций, любой из которых может быть прерван переключателем контекста потока.

Кроме того, ++i не всегда "добавляет значение к значению". На языке, подобном C, приращение указателя фактически добавляет размер указателя на вещь. То есть, если i является указателем на 32-байтовую структуру, ++i добавляет 32 байта. В то время как почти у всех платформ есть инструкция "increment value at memory address", которая является атомарной, не все имеют инструкцию "добавить произвольное значение для значения в памяти".

Ответ 3

Они обе небезопасны.

ЦП не может выполнять математику непосредственно с памятью. Он делает это косвенно, загружая значение из памяти и выполняя математику с помощью регистров CPU.

я ++

register int a1, a2;

a1 = *(&i) ; // One cpu instruction: LOAD from memory location identified by i;
a2 = a1;
a1 += 1; 
*(&i) = a1; 
return a2; // 4 cpu instructions

++ я

register int a1;

a1 = *(&i) ; 
a1 += 1; 
*(&i) = a1; 
return a1; // 3 cpu instructions

В обоих случаях существует условие гонки, которое приводит к непредсказуемому значению i.

Например, допустим, что существует два параллельных потока ++ i, каждый из которых использует регистр a1, b1 соответственно. И, с переключением контекста, выполняемым следующим образом:

register int a1, b1;

a1 = *(&i);
a1 += 1;
b1 = *(&i);
b1 += 1;
*(&i) = a1;
*(&i) = b1;

В результате я не становится я + 2, он становится я + 1, что неверно.

Чтобы исправить это, moden CPU предоставляют некоторые инструкции LOCK, UNLOCK cpu в течение интервала, когда отключено переключение контекста.

В Win32 используйте InterlockedIncrement(), чтобы сделать я ++ для обеспечения безопасности потоков. Это намного быстрее, чем полагаться на мьютекс.

Ответ 4

Если вы делитесь даже потоком между потоками в многоядерной среде, вам нужны надлежащие барьеры памяти. Это может означать использование взаимосвязанных инструкций (см., Например, InterlockedIncrement в win32) или использование языка (или компилятора), который обеспечивает определенные потокобезопасные гарантии. С инструкцией по настройке уровня процессора и кэшами и другими проблемами, если у вас нет этих гарантий, не допускайте, чтобы все, что было разделено между потоками, было безопасным.

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

Ответ 5

Если вам нужен атомный приращение в С++, вы можете использовать библиотеки С++ 0x (тип данных std::atomic) или что-то вроде TBB.

Было время, когда руководство по кодированию GNU заявляло, что обновление типов данных, которые вписываются в одно слово, было "обычно безопасным", но этот совет неверен для SMP-машин, неправильный для некоторых архитектур, и неправильный, когда используя оптимизирующий компилятор.


Чтобы прояснить комментарий "обновить однотипный тип данных":

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

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

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

ПРИМЕЧАНИЕ. В моем первоначальном ответе также говорилось, что 64-битная архитектура Intel была нарушена при работе с 64-битными данными. Это неверно, поэтому я отредактировал ответ, но мои исправленные процессоры PowerPC были сломаны. Это верно при чтении непосредственных значений (т.е. констант) в регистры (см. два раздела с надписью "Загрузка указателей" в листинге 2 и листинг 4). Но есть инструкция по загрузке данных из памяти за один цикл (lmw), поэтому я удалил эту часть своего ответа.

Ответ 6

В x86/Windows на C/С++ вы не должны предполагать, что он потокобезопасен. Вы должны использовать InterlockedIncrement() и InterlockedDecrement() если вам требуются атомные операции.

Ответ 7

Если ваш язык программирования ничего не говорит о потоках, но работает на многопоточной платформе, как может любая конструкция языка быть потокобезопасной?

Как указывали другие, вам необходимо защитить любой многопоточный доступ к переменным по специфическим для платформы вызовам.

Есть библиотеки, которые абстрагируют от специфики платформы, а предстоящий стандарт С++ адаптировал его модель памяти для борьбы с потоками (и, следовательно, может гарантировать безопасность потоков).

Ответ 8

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

При увеличении значения в памяти аппаратное обеспечение выполняет операцию "чтение-изменение-запись": оно считывает значение из памяти, увеличивает его и записывает обратно в память. Аппаратное обеспечение x86 не имеет возможности напрямую увеличивать объем памяти; ОЗУ (и кэши) может читать и сохранять значения, а не изменять их.

Теперь предположим, что у вас есть два отдельных ядра, либо в отдельных сокетах, либо для совместного использования одного сокета (с общим кешем или без него). Первый процессор считывает значение, и до того, как он сможет записать обновленное значение, второй процессор считывает его. После того, как оба процессора напишут значение обратно, оно будет увеличиваться только один раз, а не дважды.

Есть способ избежать этой проблемы; Процессоры x86 (и большинство многоядерных процессоров, которые вы найдете) способны обнаружить такой конфликт в аппаратном обеспечении и упорядочить его так, чтобы вся последовательность чтения-изменения-записи становилась атомарной. Однако, поскольку это очень дорого, это делается только при запросе кода на x86 обычно с помощью префикса LOCK. Другие архитектуры могут сделать это другими способами, с аналогичными результатами; например, связанные с загрузкой/хранилищем-условные и атомные сравнения и замены (последние процессоры x86 также имеют этот последний).

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

Лучший способ - использовать атомарные примитивы (если ваш компилятор или библиотеки есть), или сделать инкремент непосредственно в сборке (используя правильные атомарные инструкции).

Ответ 9

Никогда не предполагайте, что приращение будет скомпилировано до атомной операции. Используйте InterlockedIncrement или любые аналогичные функции на вашей целевой платформе.

Изменить: я просто просмотрел этот конкретный вопрос, и приращение на X86 является атомарным на однопроцессорных системах, но не на многопроцессорных системах. Использование префикса блокировки может сделать его атомарным, но гораздо более переносимым, чтобы использовать InterlockedIncrement.

Ответ 10

В стандарте С++ 1998 года нечего сказать о потоках, хотя следующий стандарт (из-за этого или следующего). Поэтому вы не можете сказать ничего разумного о потокобезопасности операций без ссылки на реализацию. Это не только используемый процессор, но и комбинация компилятора, ОС и модели потока.

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

Ничто не является потокобезопасным, если у вас нет документации, в которой говорится, что для конкретной системы вы используете.

Ответ 11

В соответствии с этим ассемблером сборки на x86, вы можете добавить в регистр регистр в ячейку памяти, поэтому потенциально ваш код может атомарно выполнять '++ i' ou 'i ++'. Но, как сказано в другом сообщении, C ansi не применяет атомарность к '++' opération, поэтому вы не можете быть уверены в том, что будет генерировать ваш компилятор.

Ответ 12

Я думаю, что если выражение "i ++" является единственным в утверждении, оно эквивалентно "++ i", компилятор достаточно умен, чтобы не сохранять временное значение и т.д. Поэтому, если вы можете использовать их взаимозаменяемо ( в противном случае вы не будете спрашивать, какой из них использовать), это не имеет значения, какой бы вы ни использовали, поскольку они почти одинаковы (за исключением эстетики).

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

Если вы хотите поэкспериментировать самостоятельно, напишите программу, в которой N потоков одновременно увеличивают общую переменную M раз каждый... если значение меньше N * M, тогда некоторое приращение было перезаписано. Попробуйте это с предварительным и постинкрестным и расскажите нам: -)

Ответ 13

Для счетчика я рекомендую использовать идиому сравнения и подкачки, которая является неблокирующей и потокобезопасной.

Здесь он находится в Java:

public class IntCompareAndSwap {
    private int value = 0;

    public synchronized int get(){return value;}

    public synchronized int compareAndSwap(int p_expectedValue, int p_newValue){
        int oldValue = value;

        if (oldValue == p_expectedValue)
            value = p_newValue;

        return oldValue;
    }
}

public class IntCASCounter {

    public IntCASCounter(){
        m_value = new IntCompareAndSwap();
    }

    private IntCompareAndSwap m_value;

    public int getValue(){return m_value.get();}

    public void increment(){
        int temp;
        do {
            temp = m_value.get();
        } while (temp != m_value.compareAndSwap(temp, temp + 1));

    }

    public void decrement(){
        int temp;
        do {
            temp = m_value.get();
        } while (temp > 0 && temp != m_value.compareAndSwap(temp, temp - 1));

    }
}

Ответ 14

Бросьте я в локальное хранилище потоков; он не является атомарным, но тогда это не имеет значения.

Ответ 15

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

Не зная языка, ответ должен проверить его.