Я создаю простой 2D игровой движок и хочу обновить и визуализировать спрайты в разных потоках, чтобы узнать, как это делается.
Мне нужно синхронизировать поток обновления и рендер. В настоящее время я использую два атомных флага. Рабочий процесс выглядит примерно так:
Thread 1 -------------------------- Thread 2
Update obj ------------------------ wait for swap
Create queue ---------------------- render the queue
Wait for render ------------------- notify render done
Swap render queues ---------------- notify swap done
В этой настройке я ограничиваю FPS потока рендеринга FPS потока обновления. Кроме того, я использую sleep()
для ограничения как рендеринга, так и обновления FPS потока до 60, поэтому две функции ожидания не будут ждать много времени.
Проблема в:
Среднее использование процессора составляет около 0,1%. Иногда доходит до 25% (в четырехъядерном ПК). Это означает, что поток ожидает другого, потому что функция wait является циклом while с функцией test и set, а цикл while будет использовать все ресурсы вашего ЦП.
Мой первый вопрос: есть ли другой способ синхронизации двух потоков? Я заметил, что std::mutex::lock
не используйте процессор, пока он ожидает блокировки ресурса, поэтому он не является циклом while. Как это работает? Я не могу использовать, std::mutex
потому что мне нужно будет заблокировать их в одном потоке и разблокировать в другом потоке.
Другой вопрос: Поскольку программа работает всегда со скоростью 60 FPS, почему иногда загрузка процессора увеличивается до 25%, а это означает, что один из двух ожиданий ждет много? (оба потока ограничены 60 кадрами в секунду, поэтому в идеале им не нужно много синхронизации).
Редактировать: Спасибо за все ответы. Сначала я хочу сказать, что я не запускаю новый поток каждый кадр для рендера. Я запускаю цикл обновления и рендеринга в начале. Я думаю, что многопоточность может сэкономить время: у меня есть следующие функции: FastAlg () и Alg (). Alg () - это мой объект обновления obj и render obj, а Fastalg () - это моя очередь отправки рендера для renderer. В одной теме:
Alg() //update
FastAgl()
Alg() //render
В два потока:
Alg() //update while Alg() //render last frame
FastAlg()
Так что, возможно, многопоточность может сэкономить время. (на самом деле это происходит в простом математическом приложении, где alg - длинный алгоритм, а быстрый - более быстрый)
Я знаю, что сон не очень хорошая идея, хотя у меня никогда не было проблем. Будет ли это лучше?
While(true)
{
If(timer.gettimefromlastcall() >= 1/fps)
Do_update()
}
Но это будет бесконечный цикл while, который будет использовать весь процессор. Могу ли я использовать сон (число <15), чтобы ограничить использование? Таким образом, он будет работать, например, со скоростью 100 кадров в секунду, а функция обновления будет вызываться всего 60 раз в секунду.
Для синхронизации двух потоков я буду использовать объект waitforsingle с createSemaphore, чтобы иметь возможность блокировать и разблокировать в другом потоке (без использования цикла while), не так ли?
Ответы:
Для простого 2D-движка со спрайтами однопоточный подход очень хорош. Но так как вы хотите научиться делать многопоточность, вы должны научиться делать это правильно.
Не
sleep
для управления частотой кадров. Никогда. Если кто-то говорит вам, ударить их.Во-первых, не все мониторы работают на частоте 60 Гц. Во-вторых, два таймера, тикающие с одинаковой скоростью, работающие бок о бок, всегда в конечном итоге выйдут из синхронизации (бросьте два шара для пинг-понга на стол с одинаковой высоты и прислушайтесь). В-третьих, дизайн не
sleep
является ни точным, ни надежным. Степень детализации может быть равна 15,6 мс (фактически, по умолчанию в Windows [1] ), а фрейм составляет только 16,6 мс при 60 к / с, что оставляет всего 1 мс для всего остального. Кроме того, трудно получить 16,6, кратное 15,6 ... Кроме того, разрешено (и иногда будет!) Возвращаться только через 30, 50, 100 мс или даже более длительное время.sleep
std::mutex
для уведомления другой темы. Это не то, для чего это.Делать
sleep
[2] . Кроме того, повторяющийся таймер правильно учитывает время (включая время, которое проходит между ними), в то время как спящий в течение 16,6 мс (или 16,6 мс минус метрическое измерение_получено) не делает этого.std::mutex
для одновременного доступа к ресурсу только одного потока («взаимно исключая») и для соответствия странной семантикеstd::condition_variable
.std::condition_variable
для блокировки другого потока, пока не выполнится какое-либо условие. Семантикаstd::condition_variable
с этим дополнительным мьютексом, по общему признанию, довольно странная и искаженная (в основном по историческим причинам, унаследованным от потоков POSIX), но условная переменная является правильным примитивом для использования в том, что вы хотите.В случае, если вы находите
std::condition_variable
слишком странным, чтобы чувствовать себя комфортно с ним, вы можете вместо этого просто использовать событие Windows (немного медленнее) или, если вы смелы, создать собственное простое событие вокруг NtKeyedEvents (включает в себя страшные вещи низкого уровня). Поскольку вы используете DirectX, вы все равно привязаны к Windows, поэтому потеря переносимости не должна быть большой проблемой.[1] Да, вы можете установить скорость планировщика до 1 мс, но это не одобряется, поскольку вызывает намного больше переключений контекста и потребляет намного больше энергии (в мире, где все больше и больше устройств являются мобильными устройствами). Это также не решение проблемы, поскольку оно все еще не делает сон более надежным.
[2] Таймер повысит приоритет потока, что позволит ему прерывать другой поток с равным приоритетом в середине кванта и планироваться первым, что является квази-RT поведением. Это, конечно, не правда RT, но это очень близко. Пробуждение из спящего режима означает лишь то, что поток готов к планированию на какое-то время, когда бы это ни было.
источник
Я не уверен, чего вы хотите достичь, ограничив FPS для обновления и рендеринга до 60. Если вы ограничите их одним и тем же значением, вы могли бы просто поместить их в один и тот же поток.
Цель разделения Update и Render в разных потоках состоит в том, чтобы оба «почти» не зависели друг от друга, чтобы графический процессор мог отображать 500 FPS, а логика обновления по-прежнему работала со скоростью 60 FPS. При этом вы не добьетесь очень высокого прироста производительности.
Но ты сказал, что просто хотел знать, как это работает, и это нормально. В C ++ мьютекс - это специальный объект, который используется для блокировки доступа к определенным ресурсам для других потоков. Другими словами, вы используете мьютекс, чтобы сделать разумные данные доступными только одному потоку одновременно. Для этого достаточно просто:
Источник: http://en.cppreference.com/w/cpp/thread/mutex
РЕДАКТИРОВАТЬ : Убедитесь, что ваш мьютекс является классом или файлом, как в приведенной ссылке, иначе каждый поток создаст свой собственный мьютекс, и вы ничего не добьетесь.
Первый поток, блокирующий мьютекс, будет иметь доступ к коду внутри. Если второй поток попытается вызвать функцию lock (), он будет блокироваться, пока первый поток не разблокирует ее. Так что мьютекс - это блокирующая функция, в отличие от цикла while. Функции блокировки не будут оказывать нагрузку на процессор.
источник
std::lock_guard
или аналогичные, а не.lock()
/.unlock()
. RAII не только для управления памятью!