Подтвердить что ты не робот

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

У меня есть два потока. Они имеют массив. Один из потоков добавляет новые элементы в массив (и удаляет их), а другой использует этот массив (только операции чтения). Нужно ли мне блокировать массив, прежде чем добавлять/удалять его/из него или читать из него?

Дополнительная информация:

  • Мне нужно будет продолжать итерацию по всему массиву в другом потоке. Здесь нет операций записи, как упоминалось ранее. "Просто сканировать что-то вроде кругового буфера фиксированного размера"
  • В таких случаях легко использовать блокировку. Однако блокировки могут быть очень медленными. Я не хотел использовать блокировки, если их использование можно избежать. Также, как выяснилось из обсуждений, может быть необязательно (на самом деле это не так) блокировать все операции над массивом. Достаточно просто заблокировать управление итератором для массива (счетчик, который будет использоваться другим потоком)

Я не думаю, что вопрос "слишком широк". Если это все равно, дайте мне знать. Я знаю, что вопрос не идеален. Мне пришлось объединить как минимум 3 ответа, чтобы решить этот вопрос, который говорит о том, что большинство людей не смогли полностью понять все проблемы и были вынуждены выполнять некоторые предположения. Но большинство из них вышло из комментариев, которые я попытался включить в вопрос. Ответы помогли мне решить мою проблему довольно объективно, и я думаю, что предоставленные здесь ответы являются весьма полезным ресурсом для тех, кто начинает многопоточность.

4b9b3361

Ответ 1

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

Итак, вы должны использовать какой-то механизм синхронизации, например:

Ответ 2

Если вы пишете и читаете из одного места из нескольких потоков, вам нужно будет выполнить блокировку или использовать атомику. Мы можем это увидеть, посмотрев стандарт проекта C11 (стандарт С++ 11 выглядит почти идентичным, эквивалентный раздел будет 1.10) говорит следующее в разделе 5.1.2.4 Многопоточные исполнения и расписания данных:

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

и

Выполнение программы содержит гонку данных, если она содержит два конфликтующие действия в разных потоках, по крайней мере один из которых не является атомный, и не происходит другого. Любая такая гонка данных приводит к поведению undefined.

и:

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

Если бы вы просто добавляли данные в массив, а затем в мир С++, то std:: atomic было бы достаточно, так как вы можете добавить больше элементов, а затем атомарно увеличивать индекс. Но так как вы хотите вырастить и сжать массив, вам нужно будет использовать мьютекс, в мире С++ std:: lock_guard будет типичным выбор.

Ответ 3

Один из потоков добавляет новые элементы в массив [...], а другой [читает] этот массив

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

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

int lastValid = 0;
int shared[MAX];
...
int count = toAddCount;
// Add the new data
for (int i = lastValid ; count != 0 ; count--, i++) {
    shared[i] = new_data(...);
}
// Lock a mutex before modifying lastValid
// You need to use the same mutex to protect the read of lastValid variable
lock_mutex(lastValid_mutex);
lastValid += toAddCount;
unlock_mutex(lastValid_mutex);

Причина этого в том, что когда вы выполняете запись в shared[] за пределами заблокированной области, читатель не "смотрит" мимо индекса lastValid. Как только запись будет завершена, вы заблокируете мьютекс, который обычно вызывает сброс кэша процессора, поэтому запись в shared[] будет завершена до того, как читателю будет разрешено видеть данные.

Ответ 4

Чтобы ответить на ваш вопрос: возможно.

Проще говоря, способ постановки вопроса не дает достаточной информации о необходимости блокировки.

В большинстве стандартных случаев использования ответ будет да. И большинство ответов здесь очень хорошо освещают этот случай.

Я расскажу о другом случае.

Когда вам не понадобится блокировка с предоставленной информацией?

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

Будут ли писать данные когда-либо неатомными? Смысл будет ли запись данных когда-либо приводить к "разрыву данных"? Если ваши данные представляют собой одно 32-битное значение в системе x86, а ваши данные выровнены, тогда у вас будет случай, когда ваши данные уже являются атомарными. Можно с уверенностью предположить, что если ваши данные имеют размер, отличный от размера указателя (4 байта на x86, 8 на x64), то ваши записи не могут быть атомарными без блокировки.

Будет ли размер вашего массива когда-либо изменяться таким образом, который требует перераспределения? Если ваш читатель просматривает ваши данные, данные внезапно исчезнут (память была "удалена" d )? Если ваш читатель не учитывает это (маловероятно), вам понадобится блокировка, если возможно перераспределение.

Когда вы записываете данные в ваш массив, нормально ли читатель видит старые данные?

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

Наверное, безопаснее использовать блокировку, так как вы были не уверены, чтобы задать этот вопрос. Но, если вы хотите поиграть с краем, где вам не нужен замок... там вы идете:)

Ответ 5

Блокировка? Нет. Но вам нужен механизм синхронизации.

То, что вы описываете, звучит ужасно, как очередь SPSC (Single Producer Single Consumer), из которых есть тонны безблокированных реализаций, в том числе один в Boost.Lockfree

Общий способ работы заключается в том, что под обложками имеется круглый буфер, содержащий ваши объекты и индекс. Писатель знает последний индекс, который он написал, и если ему нужно написать новые данные, он (1) записывает в следующий слот, (2) обновляет индекс, устанавливая индекс в предыдущий слот + 1, а затем (3) сигнализирует читателю. Затем читатель читает до тех пор, пока не попадет в индекс, который не содержит индекс, который он ожидает, и ждет следующего сигнала. Deletes неявно, поскольку новые элементы в буфере перезаписывают предыдущие.

Вам нужен способ атомарного обновления индекса, который предоставляется атомарным < > и имеет прямую аппаратную поддержку. Вам нужен способ, чтобы писатель мог сигнализировать читателю. Вам также понадобятся заграждения памяти в зависимости от платформы s.t. (1-3) происходят по порядку. Вам не нужно ничего такого тяжелого, как блокировка.

Ответ 6

"Классический" POSIX действительно нуждается в блокировке для такой ситуации, но это излишне. Вы просто должны убедиться, что чтение и запись являются атомарными. C и С++ имеют это на языке с их версий версий 2011 года. Компиляторы начинают реализовывать его, по крайней мере, последние версии Clang и GCC.

Ответ 7

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

Ответ 8

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

Ответ 9

Если это массив фиксированного размера, и вам не нужно связывать ничего лишнего, как индексы, написанные/обновленные, то вы можете избежать взаимного исключения с предостережением, которое может видеть читатель:

  • нет обновлений вообще
    • Если ваш порядок памяти достаточно расслаблен, чтобы это произошло, вам нужно ограждение магазина в писателе и загрузочный забор у потребителя, чтобы исправить его.
  • частичная запись
    • Если сохраненный тип не является атомарным на вашей платформе (int обычно должен быть)
    • или ваши значения не выровнены, и особенно если они могут охватывать строки кэша

Все это зависит от вашей платформы, хотя это может повлиять на аппаратное обеспечение, ОС и компилятор. Вы не сказали нам, кто они.

Портативное решение С++ 11 - использовать массив atomic<int>. Вам все равно нужно решить, какие ограничения порядка памяти вам потребуются, и что это означает для правильности и производительности на вашей платформе.

Ответ 10

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

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

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

Или, даже если компилятор/оптимизатор решает переключить порядок выполнения для назначения элементов массива и счетчиков приращений/декрементов, вы теряете.

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