У меня большой массив (3e9 элементов) данных, и я обновляю его значение в нескольких потоках. Я только что узнал, что есть условия гонки.
Я думаю, что нет необходимости блокировать всю функцию, так как элементы не зависят друг от друга, обновление на data[1]
а также data[234]
может быть безопасно сделано в то же время.
Я также нашел самый значительный бит каждого элемента в data[]
никогда не будет использоваться. Безопасно ли реализовывать атомарную встроенную блокировку GCC для этого бита?
Мой код выглядит следующим образом, но, похоже, он заходит в тупик.
const unsigned short LOCK_MASK = 1<<15;
unsigned short * lock = &(data[position]);
unsigned short oldLock, newLock;
//lock
do {
oldLock = *lock;
newLock = oldLock ^ LOCK_MASK;
} while ((oldLock & LOCK_MASK) || !__sync_bool_compare_and_swap(lock, oldLock, newLock));
//update data[position] here
...
...
...
//unlock
*lock ^= LOCK_MASK;
Я тоже читал этот пост (Легкие спин-блокировки, созданные из атомных операций GCC?) и добавил volatile
на моем data
РЕДАКТИРОВАТЬ В моем дизайне 0 означает разблокирован, а 1 означает заблокирован
Ваш код содержит ряд данных гонок, в том числе oldLock = *lock
и разблокировать бит *lock ^= LOCK_MASK
,
который не может синхронизировать ваши обновления с другими ядрами из-за отсутствия барьера выпуска.
Помните, что помимо блокировки сегмента массива для доступа на запись, вам также необходимо заблокировать этот сегмент для доступа на чтение, поскольку операции чтения и записи должны быть синхронизированы.
Безопасно ли реализовывать атомарную встроенную блокировку GCC для этого бита?
Несколько битов требуются, если вы хотите выразить отдельные состояния для чтения и записи (разблокирован, заблокирован на чтение x N, заблокирован на запись).
Один бит ограничивает блокировку до 2 состояний, запертый а также разблокирована, который, основываясь на вашем коде, может быть реализован с помощью:
const unsigned short LOCK_MASK = 1<<15;
void lock_array_segment(int position)
{
unsigned short *lock = &data[position]; // global array
unsigned short oldLock, newLock;
do {
oldLock = __atomic_load_n (lock, __ATOMIC_RELAXED);
newLock = oldLock | LOCK_MASK; // set bit
} while ((oldLock & LOCK_MASK) || !__sync_bool_compare_and_swap(lock, oldLock, newLock));
}void unlock_array_segment(int position)
{
unsigned short *lock = &data[position]; // global array
unsigned short oldLock, newLock;
oldLock = __atomic_load_n (lock, __ATOMIC_RELAXED);
newLock = oldLock & ~LOCK_MASK; // clear bit
__atomic_store_n (lock, newLock, __ATOMIC_RELEASE);
}
Документация для __sync_bool_compare_and_swap
говорит В большинстве случаев эти встроенные функции считаются полным барьером.. Здесь вам нужен барьер для приобретения, так что он должен быть покрыт.
Поскольку ваш подход основан на спин-блокировке, он не работает, если вы хотите удерживать блокировку чтения в течение более длительного времени. В этом случае рассмотрим более простой подход с отдельным мьютексом для каждого сегмента в массиве данных, который необходимо заблокировать.
Если вы хотите предоставить доступ нескольким читателям, рассмотрите возможность использования std::shared_mutex
(C ++ 17) или boost::shared_mutex
,
Вы должны рассмотреть более стандартные способы блокировки (в C ++ 11 или лучше).
Возможно, начните с чтения некоторых Учебник Pthread (по крайней мере, для концепций, объясненных там).
Прочитать о атомные операции а также поддержка потоков в C ++.
Вы можете эвристически рассмотреть возможность наличия мьютекса для каждого сегмента последовательных 1024 (или какой-либо другой степени двух) элементов.
Вы могли бы рассмотреть производитель-потребитель подход.