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

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

if (std::find(allow_list->begin(), allow_list->end(), target_string) != allow_list->end()){
    allow = true;
}

Теперь другой поток может сделать что-то вроде этого

// Some operation to a vector called allow_list_updated
allow_list = allow_list_updated;

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

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

Refugnic Eternium 14.02.2023 07:16

find фактически является циклом, повторяющим вектор. AFAICT, вы настраиваете сценарий, в котором вы потенциально заменяете весь массив, управляемый allow_list, новым в середине цикла, делая недействительными все старые итераторы и освобождая память старого, к которому теперь недопустимые итераторы обращались. Кажется, что «разбить и сжечь» — это именно то, о чем идет речь.

Avi Berger 14.02.2023 07:17

Я смущен. Так что, если итераторы станут недействительными? Каждый раз, когда вызывается find, он вызывает begin() и end() для allow_list, и результат поиска будет основываться на allow_list.

kiner_shah 14.02.2023 07:24
godbolt.org/z/sqPrG8qxo - Краша не вижу.
kiner_shah 14.02.2023 07:31

find — это конструкция исходного кода C++. Это не машинная инструкция или атомарность. Это сокращение от цикла, который постоянно обновляет итератор и проверяет условие. Если у него есть 100 элементов для итерации, и как только он проходит первые 50, ваш другой поток отключает итераторы и уничтожает массив, БУМ! Это зависит от времени. То, что это не происходит в одном (или 200) прогоне, не означает, что этого не может произойти в следующем. Вот почему у нас есть контроль параллелизма.

Avi Berger 14.02.2023 08:01

@kiner_shah вот

n. m. 14.02.2023 14:27

@н.м. спасибо за пример, теперь понятно.

kiner_shah 15.02.2023 07:09
Стоит ли изучать PHP в 2023-2024 годах?
Стоит ли изучать PHP в 2023-2024 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
0
7
88
2
Перейти к ответу Данный вопрос помечен как решенный

Ответы 2

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

я бы также рекомендовал либо std::swap , либо std::move вместо allow_list = allow_list_updated; в зависимости от того, можно ли отказаться от allow_list_updated после изменения; это намного быстрее. если вы часто обновляете этот список, вы, вероятно, захотите использовать std::swap и где-то хранить два списка в области видимости и просто .clear() и std::swap() при каждом их обновлении. это будет бороться с фрагментацией памяти. пример:

class updater
{
public:
     std::vector<std::string> allowed;
     std::vector<std::string> allowed_updated;

     void update()
     {
          // @TODO: do your update to this->allowed_updated, use this->allowed_updated.reserve() if you know how many items there will be

          std::swap(this->allowed, this->allowed_updated);
          this->allowed_updated.clear();
     }
};

Спасибо, не знал о функции подкачки, но безопасен ли этот поток? Allow_updated можно отбросить, меня волнует только сам allow_list.

Baiqing 14.02.2023 07:51

Во внутренней реализации swap(move internal) используется как стандартом::vector, так и boost::vector. Таким образом, operator= на самом деле является std::swap для векторов. (старый вектор теперь мусор), что-то вроде: _M_move_assign(std::move(__x), __bool_constant<__move_storage>());

H.M 14.02.2023 08:03

Во-первых, std::swap() не является атомарным. Во-вторых, он может проскользнуть между вызовами begin() и end() в другом потоке. В-третьих, вы очищаете вектор, который в данный момент повторяет другой поток.

n. m. 14.02.2023 14:34
Ответ принят как подходящий

У вас есть состояние гонки, и вам нужно заблокировать. Простое правило, если поток может читать переменную с неатомарной записью из другого, у вас есть гонка для этой переменной. Еще одна проблема, вам нужно заблокировать все вектора. Если у вас много прочтений и редких записей, std::shared_mutex может быть хорошей идеей. Если allow_list периодически обновляется только с краев, список будет лучшим вариантом для allow_list = allow_list_updated, так как чтобы поменять местами список, вам нужно поменять местами голову и хвост. Еще одним потенциальным преимуществом списка является отсутствие ложного обмена. Что бы вы ни делали, ваш контейнер и его защита должны быть одного класса.

Другие вопросы по теме