Вопрос о потоке без блокировок в C ++. Несколько потоков, проходящих по непрерывному массиву, но никогда не обращающихся к одним и тем же данным? - PullRequest
0 голосов
/ 19 апреля 2020

В моем игровом движке C ++ у меня есть система заданий, которая использует рабочие потоки для выполнения различных задач. Потоки привязываются к каждому доступному ядру. Недавно я пытался оптимизировать некоторые из моих системных конвейеров, максимально увеличив загрузку процессора. Вот пример псевдо-i sh кода. Это не точная копия, но ситуация похожа.

struct entityState {
  uint8 * byteBuffer; // Serialized binary data for the Entity
  uint8 * compressedData; // Compressed version of Entity data
  uint64  guid; // Unique ID
  gameTimeMS lastUpdated; // last time buffer was updated in milliseconds
  uint32 numUpdates; // Count of the number of updates
  uint32 numTimesAckedOverNetwork; // How many times client acked the data
  const char * typeData; // Type data in place of RTT
  bool markedForDelete; // Whether this object should be deleted next frame
  const char * debugData; // In debug configs, store meta data 
  // More member data but the point is made
};

// For examples sake, I have a contiguous array of entityState data
List< entityState * > entityStateList;
PopulateListWithEntityStateData(); // ~20,000 entityState ptrs on average
SortEntityStateList();
// Fire off 5 jobs each with their own worker thread
StartEntityStateJobs();

У меня есть 5 заданий, которые работают в этом списке одновременно без мьютексов или критических секций . Каждая функция задания получает доступ к массиву посредством бинарного поиска на основе критериев, таких как guid, или просто линейного поиска. Здесь подвох. Ни одна из функций задания не изменяет одни и те же данные-члены объектов entityState в entityStateList . Тем не менее, они могут задерживать один и тот же entityState ptr из-за бинарного поиска против линейного поиска, имеющего коллизии. Но, повторяю, они никогда не изменяют одни и те же данные участника одновременно. Никакие ptrs данных членов не разыменовываются одновременно в каждом потоке.

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

Другой момент, о котором я слышал, заключается в том, что причина, по которой эта настройка сработала, состоит в том, что размер структуры entityState не помещается в строку кэша и в итоге разделяет выборку данных, которая сама по себе действует как данные. Сама защита из-за разделения данных структуры на разные строки кэша. Для пояснения, скажем, верхняя половина помещается в одну строку кэша, а нижняя - в другую, а функции задания работают только с одним членом данных entityState ptr, и большую часть времени это происходит в другой строке кэша. Я не использую никакие модификаторы или операции Atomi c для данных члена, потому что ни одно задание не касается данных члена.

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

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

Вопрос в том ... возможно ли сверхнизкое cra * sh, которое может произойти в дикой природе 1 раз из 'x'? Даже 1/1 миллион не приемлемо. Это безопасный механизм потоков lockless для параллельного выполнения нескольких операций над списком? Попробуйте не обращать внимания на тривиальность данных примера. Это намного сложнее в моем примере двигателя. Этот код может работать на нескольких ОС, таких как P C, Linux и консоли. Это еще не до sh, но воздействие и испытания ограничены. Я признаю, что я не эксперт низкого уровня, но это экономит драгоценное время работы. Итак, я жду, чтобы наткнуться на мину или это безопасно? Компилятор g cc версия C ++ 11. Также, пожалуйста, избегайте производительности topi c, если это не связано с многопоточностью и / или безопасностью потоков. Я знаю, что промах кэша плох.

Вопрос - Является ли потокобезопасным или нет? Если да или нет, пожалуйста, объясните, почему, если возможно, подробно. Я хотел бы поддержать свои знания низкого уровня.

1 Ответ

1 голос
/ 20 апреля 2020

@ walnut уже подробно объяснял, что «доступ к различным элементам массива гарантированно не вызовет скачки данных».

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

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

...