Потоковая безопасность записи std::vector vs plain array
Я прочитал qaru.site/info/177928/..., что ни один из контейнеров STL не является потокобезопасным для записи. Но что это значит на практике? Означает ли это, что я должен хранить записываемые данные в простых массивах?
Я ожидаю, что одновременные вызовы std::vector::push_back(element)
могут привести к несогласованным структурам данных, потому что это может повлечь изменение размера вектора. Но как насчет такого случая, когда изменение размера не задействовано:
1), используя массив:
int data[n];
// initialize values here...
#pragma omp parallel for
for (int i = 0; i < n; ++i) {
data[i] += func(i);
}
2) с помощью ` std::vector` `:
std::vector<int> data;
data.resize(n);
// initialize values here...
#pragma omp parallel for
for (int i = 0; i < n; ++i) {
data[i] += func(i);
}
Является ли первая реализация действительно лучше, чем вторая, а) с точки зрения безопасности потоков и б) с точки зрения производительности? Я бы предпочел использовать std::vector, так как я менее удобен для массивов в стиле C.
EDIT: я удалил #pragma omp atomic update
, защищающий запись.
Ответы
Ответ 1
Оба одинаково безопасны. Если ни один элемент не доступен из нескольких потоков, вы в порядке. Ваш параллельный цикл будет обращаться к каждому элементу только один раз и, следовательно, только из одного потока.
В стандартном варианте для функций-членов контейнеров не должно быть потокобезопасным. В этом случае вы используете vector<int>::operator[]
, поэтому вам нужна явная гарантия безопасности потоков для этого члена, что кажется разумным, поскольку его вызов даже на неконстантном векторе не изменяет сам вектор. Поэтому я сомневаюсь, что в этом случае есть проблема, но я не искал гарантии [edit: rici found it]. Даже если это потенциально опасно, вы можете сделать int *dataptr = &data.front()
перед циклом, а затем индексировать dataptr
вместо data
.
В стороне, этот код не гарантируется для vector<bool>
безопасным, так как это специальный случай, для которого несколько элементов сосуществуют внутри одного объекта. Это было бы безопасно для массива bool
, поскольку разные элементы этого являются разными "ячейками памяти" (1.7 в С++ 11).
Ответ 2
В С++ 11, который определяет правила для расчётов данных, описана безопасность потоков контейнеров. Соответствующим разделом стандарта является & sect; 23.2.2, пункт 2:
Несмотря на (17.6.5.9), реализации необходимо избегать расследований данных, когда содержимое содержащегося объекта в разных элементах в одной и той же последовательности, за исключением вектора <bool> , изменяется одновременно.
[Примечание: для вектора <int> x с размером больше единицы, x [1] = 5 и * x.begin() = 10 могут выполняться одновременно без гонки данных, но x [0] = 5 и * x.begin() = 10 выполняются одновременно может привести к гонке данных. В качестве исключения из общего правила для вектора <bool> y, y [0] = true может участвовать в гонке с y [1] = true. -end note]
Упомянутая секта; 17.6.5.9 по существу запрещает любую параллельную модификацию любым стандартным интерфейсом библиотеки, если это специально не разрешено, поэтому в разделе, который я цитирую, вы точно указываете, что разрешено (и это включает ваше использование).
Поскольку вопрос поднял Стив Джессоп, пункт 1 раздела " 23.2.2 явно разрешает одновременное использование []
в контейнерах последовательностей:
Чтобы избежать расчётов данных (17.6.5.9), реализации должны учитывать следующие функции: begin, end, rbegin, rend, front, back, data, find, lower_bound, upper_bound, equal_range, at и, кроме ассоциативных или неупорядоченных ассоциативных контейнеров, оператор [].
Ответ 3
Главное, что это означает, что если у вас есть несколько потоков, обращающихся к вектору, вы не можете зависеть от С++, чтобы не допустить, чтобы вы нарушили структуру данных несколькими параллельными записями. Поэтому вам нужно использовать какой-то охранник. С другой стороны, если ваша программа не использует несколько потоков, поскольку ваши примеры не кажутся вам, вы в полном порядке.
Ответ 4
В этом случае вы должны просто построить свой вектор с необходимым количеством значений? и все будет нормально работать.
std::vector<int> data(n, 0);
resize()
хорошо работает. Производительность будет одинаковой.
Причина, по которой многопоточный доступ не приведет к повреждению вектора, заключается в следующем: ваши данные расположены на своем месте и не будут двигаться оттуда. Точки OMP не будут обращаться к одному элементу за раз.