2017-01-04 5 views
1

Я планирую на написание многопоточного участия в моей игре-проекте:Требуется ли для этого кода синхронизация?

Темы A: загружает кучу объектов с диска, который занимает до нескольких секунд. Каждый загруженный объект увеличивает счетчик.

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

В коде я считаю, что это будет выглядеть следующим образом:

Counter = 0; 
Objects; 

THREAD A: 
    for (i = 0; i < ObjectsToLoad; ++i) { 
     Objects.push(LoadObject()); 
     ++Counter; 
    } 
    return; 

THREAD B: 
    ... 
    while (true) { 
     ... 
     C = Counter; 
     if (C < ObjectsToLoad) 
      RenderLoadscreen(C); 
     else 
      WorkWithObjects(Objects) 
     ... 
    } 
    ... 

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

Теперь вопрос в том, должен ли я реализовать любую механику синхронизации здесь, например, сделать счетчик атома или ввести какую-либо мьютекс или условную переменную? Дело здесь в том, что я могу спокойно жертвовать итерацией цикла до тех пор, пока счетчик не изменится. И из того, что я получаю, до тех пор, пока A записывает только значение, и B проверяет его, все в порядке.

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

+0

Вы также должны учитывать видимость/кеширование памяти. – 1000ml

ответ

2

Вы должны учитывать видимость/кеширование памяти. Без барьеров памяти это может привести к задержкам в течение нескольких секунд, пока данные не будут видны в потоке B (1).

Это относится к обоим типам данных: Counter и Objects.

Стандарт C++ 11 (2) гарантирует, что многопоточные программы выполняются правильно, только если вы не вводите условия гонки. Без синхронизации ваша программа в основном имеет неопределенное поведение (3). Однако на практике это может обойтись без этого.

Да, используйте мьютексы и синхронизируйте доступ к Counter и Objects.


(1) Это потому, что каждое ядро ​​процессора имеет свои собственные регистры и кэш. Если вы не сообщите CPU Core A, что некоторые другие Core B могут быть заинтересованы в данных, он может выполнять оптимизацию, например. оставляя данные в регистре. Core A должен записать данные в область памяти более высокого уровня (кэш L2/L3 или ОЗУ), так что Core B может загрузить изменения.

(2) Любая версия до того, как C++ 11 не заботился о многопоточности. У сторонних библиотек была поддержка мьютексов, атомистики и т. Д., Но сам язык был нитевидным.
См: C++11 introduced a standardized memory model. What does it mean? And how is it going to affect C++ programming?

(3) Проблема заключается в том, что ваш код может быть заказана (для более эффективного выполнения) на разных этапах: в компилятор, ассемблер, а также на CPU. Вы должны сообщить компьютеру, какие инструкции должны оставаться в этом порядке, добавив барьеры памяти через атомы или мьютексы. Это работает на большинстве языков.

Я бы рекомендовал смотреть это очень интересное видео о модели памяти C++ 11:
atomic<> weapons by Herb Sutter


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

+0

Не могли бы вы подробнее рассказать об этой теме применительно к C++? Из быстрого googling это выглядит как сумасшедший материал, который молчаливо подставляет UB в местах, где это невозможно даже при разумной логике. Кроме того, я не могу использовать 'volatile', чтобы запретить такое переупорядочение? – Artalus

+0

Я посмотрю, смогу ли найти ссылки, которые имею в виду. Тем не менее, вся тема совершенно независима от языка. Что касается 'volatile': http://stackoverflow.com/questions/2484980/why-is-volatile-not-considered-useful-in-multithreaded-c-or-c-programming – 1000ml

+0

@Artalus: Отредактировано. Также обратите внимание, что 'volatile' в C++ означает нечто иное, чем в Java. – 1000ml

0

Состояние гонки обычно происходит только тогда, когда две нити пытаются неатомически считывать-модифицировать-записывать одновременно одну и ту же дату. В этом случае записывается только один поток (поток A), в то время как другой поток читается (поток B).

Единственная «некорректность», с которой вы столкнулись, - это, как вы сказали, если объект был загружен, но счетчик не был увеличен. Это приводит к тому, что B считывает устаревшие данные, так как операция load-and-increment не выполнялась атомарно.

Если вы не возражаете против этой невинной аномалии, то она работает нормально. :)

Если это вас раздражает, вам нужно выполнить все операторы load-and-increment за один раз (используя блокировки или любой другой примитив синхронизации).

Смежные вопросы