별도의 스레드에서 업데이트 및 렌더링


12

간단한 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으로 제한하는 데 사용 하므로 두 대기 함수는 많은 시간을 기다리지 않습니다.

문제는:

평균 CPU 사용량은 약 0.1 %입니다. 때로는 쿼드 코어 PC에서 25 %까지 올라갑니다. 대기 기능은 테스트 및 설정 기능이있는 while 루프이므로 while 루프는 모든 CPU 리소스를 사용하기 때문에 스레드가 다른 스레드를 기다리고 있음을 의미합니다.

내 첫 번째 질문은 두 스레드를 동기화하는 다른 방법이 있습니까? std::mutex::lock리소스를 잠그기를 기다리는 동안 CPU를 사용하지 않아서 while 루프가 아닌 것을 알았습니다 . 어떻게 작동합니까? std::mutex한 스레드에서 잠금을 해제하고 다른 스레드에서 잠금을 해제해야하기 때문에 사용할 수 없습니다 .

다른 질문은; 프로그램이 항상 60 FPS로 실행되기 때문에 때때로 CPU 사용량이 25 %로 증가하는 이유는 두 대기 중 하나가 많이 기다리고 있다는 의미입니다. (두 스레드는 모두 60fps로 제한되므로 이상적으로 많은 동기화가 필요하지 않습니다).

편집 : 모든 답장을 보내 주셔서 감사합니다. 먼저 렌더링을 위해 각 프레임마다 새 스레드를 시작하지 않는다고 말하고 싶습니다. 처음에는 업데이트와 렌더 루프를 모두 시작합니다. 멀티 스레딩은 시간을 절약 할 수 있다고 생각합니다 .FastAlg () 및 Alg () 함수가 있습니다. Alg ()는 내 업데이트 obj 및 렌더 obj이며 Fastalg ()는 "렌더링 큐 보내기"렌더러 ""입니다. 단일 스레드에서 :

Alg() //update 
FastAgl() 
Alg() //render

두 실에서 :

Alg() //update  while Alg() //render last frame
FastAlg() 

따라서 멀티 스레딩은 같은 시간을 절약 할 수 있습니다. (실제로 간단한 수학 응용 프로그램에서는 alg가 긴 알고리즘 amd fastalg 더 빠른 알고리즘입니다)

나는 결코 문제가 없지만 수면은 좋지 않다는 것을 알고 있습니다. 이것이 더 나을까요?

While(true) 
{
   If(timer.gettimefromlastcall() >= 1/fps)
   Do_update()
}

그러나 이것은 모든 CPU를 사용하는 무한 while 루프가 될 것입니다. 사용을 제한하기 위해 sleep (숫자 <15)을 사용할 수 있습니까? 이러한 방식으로 예를 들어 100fps에서 실행되며 업데이트 기능은 초당 60 회만 호출됩니다.

두 개의 스레드를 동기화하기 위해 createSemaphore와 함께 waitforsingleobject를 사용하므로 다른 스레드에서 잠그고 잠금을 해제 할 수 있습니다 (while 루프를 사용하여 엉클 림).


5
"이 경우 멀티 스레딩이 쓸모 없다고 말하지 말고 어떻게해야하는지 배우고 싶습니다." 이 경우 제대로 학습해야합니다. 즉, (a) sleep ()을 사용하여 프레임을 거의 제어하지 않습니다. , 결코 지금까지 , 그리고 (b) 피할 스레드 당 구성 요소 디자인과 피할 실행 록 스텝, 작업 대기열에서 작업 및 핸들 작업에 대신 분할 작업.
데이먼

1
@Damon (a) sleep ()은 프레임 속도 메커니즘으로 사용될 수 있으며 실제로는 훨씬 인기가 있지만 훨씬 더 나은 옵션이 있음에 동의해야합니다. (b) 여기서 사용자는 두 개의 다른 스레드에서 업데이트와 렌더링을 분리하려고합니다. 이것은 게임 엔진에서 정상적인 분리이며 "구성 요소 별 스레드"가 아닙니다. 명확한 이점을 제공하지만 잘못 수행하면 문제가 발생할 수 있습니다.
Alexandre Desbiens

@AlphSpirit : 뭔가 "일반적인"이라는 사실이 아니라는 것을 의미하지 않는다 잘못 . 다양한 타이머를 사용하지 않아도, 기존의 모든 소비자 시스템 에서 디자인 당 신뢰성이 아니더라도 최소한 하나의 인기있는 데스크탑 운영 체제에서의 수면의 세분성만으로도 충분 합니다. 설명과 같이 업데이트와 렌더를 두 개의 스레드로 분리하는 것이 현명하지 못하고 너무 오래 걸리는 것보다 더 많은 문제를 일으키는 이유를 설명하십시오. OP의 목표는 수행 방법배우는 것으로 명시되어 있으며 올바르게 수행하는 방법을 배워야 합니다 . 현대의 MT 엔진 설계에 관한 많은 기사.
데이먼

@Damon 나는 그것이 대중적이거나 일반적이라고 말했을 때 그것이 옳았다 고 말하려는 의도는 없었습니다. 나는 그것이 많은 사람들에 의해 사용되었다는 것을 의미했습니다. "... 더 나은 옵션이 있다는 데 동의해야한다"는 것은 실제로 시간을 동기화하는 좋은 방법이 아니라는 것을 의미했습니다. 오해해서 죄송합니다.
Alexandre Desbiens

@AlphSpirit : 걱정할 필요가 없습니다 :-) 세상은 많은 사람들이하는 것들로 가득 차 있습니다 (그리고 항상 좋은 이유는 아닙니다). 그러나 학습을 시작할 때 가장 명백하게 잘못된 것을 피하려고 노력해야합니다.
데이먼

답변:


25

스프라이트가있는 간단한 2D 엔진의 경우 단일 스레드 방식이 완벽합니다. 그러나 멀티 스레딩을 수행하는 방법을 배우려면 올바르게 수행하는 방법을 배워야합니다.

하지 마라

  • 잠금 단계를 다소 실행하는 2 개의 스레드를 사용하여 여러 스레드로 단일 스레드 동작을 구현하십시오. 이것은 동일한 수준의 병렬 처리 (영)를 갖지만 컨텍스트 전환 및 동기화에 대한 오버 헤드를 추가합니다. 또한 논리는 이해하기 어렵습니다.
  • sleep프레임 속도를 제어하는 ​​데 사용 합니다. 못. 누군가 당신에게 말하면 명중하십시오.
    첫째, 모든 모니터가 60Hz에서 실행되는 것은 아닙니다. 둘째, 동일한 속도로 나란히 두 개의 타이머가 작동하면 결국 동기화되지 않습니다 (같은 높이에서 테이블에 두 개의 탁구 공을 떨어 뜨리고 들어). 셋째, sleep디자인에 의해 정확하고도 신뢰성도. 세분성은 15.6ms (실제로 Windows [1] 의 기본값)만큼 나쁠 수 있으며 프레임은 60fps에서 16.6ms에 불과하므로 다른 모든 항목에는 1ms가 남습니다. 또한 16.6을 15.6의 배수로 만드는 것은 어렵습니다.
    또한 sleep30 또는 50 또는 100ms 또는 더 긴 시간 후에 만 ​​(때로는!) 반환 할 수 있습니다.
  • std::mutex다른 스레드에 알리는 데 사용하십시오 . 이것이 목적이 아닙니다.
  • TaskManager가 진행 상황, 특히 코드, 사용자 모드 드라이버 또는 다른 곳에서 사용될 수있는 "25 % CPU"와 같은 숫자로 판단하는 데 능숙하다고 가정하십시오.
  • 상위 레벨 구성 요소 당 하나의 스레드를 갖습니다 (물론 예외가 있습니다).
  • 작업마다 "임의 시간"으로 임시 스레드를 작성하십시오. 스레드를 만드는 것은 놀랍게도 비쌀 수 있으며, 말한 내용을 잘 수행하기 전에 (특히 많은 DLL 이로 드 된 경우) 놀랍게도 오랜 시간이 걸릴 수 있습니다.

하다

  • 멀티 스레딩을 사용하여 작업을 가능한 한 비동기 적 으로 실행하십시오 . 스레딩의 주요 아이디어는 속도가 아니라 병렬 작업을 수행하는 것입니다 (따라서 시간이 더 걸리더라도 전체의 합은 여전히 ​​적습니다).
  • 수직 동기를 사용하여 프레임 속도를 제한하십시오. 이것이 유일하고 올바른 방법입니다. 디스플레이 드라이버의 제어판에서 사용자를 무시하면 ( "강제 해제") 그렇게하십시오. 결국 그것은 당신의 컴퓨터가 아니라 그의 컴퓨터입니다.
  • 규칙적인 간격으로 무언가를 "틱"해야하는 경우 timer를 사용하십시오 . 타이머는 sleep[2]에 비해 훨씬 더 나은 정확도와 신뢰성을 갖는 이점이 있습니다. 또한 되풀이 타이머는 시간을 정확하게 고려하지만 (사이를 지나는 시간 포함) 16.6ms (또는 16.6ms-측정 _ 시간 _ 경과)에서 절전 상태를 유지하는 것은 아닙니다.
  • 고정 된 시간 단계에서 숫자 통합을 포함하는 실행 물리학 시뮬레이션 (또는 폭발 할 것이다 방정식을!), 단계 보간 그래픽 (이 있습니다 별도의 구성 요소 별 스레드에 대한 변명이 될뿐만 아니라없이 수행 할 수 있습니다).
  • 사용 std::mutex시간 ( "상호 제외")에 하나의 스레드 접근 자원을 가지고, 그리고 이상한 의미 준수하는 std::condition_variable.
  • 스레드가 리소스를 놓고 경쟁하지 않도록하십시오. 필요한만큼만 잠그십시오 (그러나 그 이하도 아닙니다). 꼭 필요한만큼만 자물쇠를 잡으십시오.
  • 스레드간에 읽기 전용 데이터를 공유하지만 (캐시 문제 및 잠금 필요 없음) 동시에 데이터를 수정하지 마십시오 (동기화 및 캐시 종료 필요). 여기에는 다른 사람이 읽을 수있는 위치 근처에 있는 데이터 수정이 포함됩니다 .
  • std::condition_variable일부 조건이 충족 될 때까지 다른 스레드를 차단하는 데 사용하십시오 . std::condition_variable여분의 뮤텍스가 있는 의미는 분명히 이상하고 왜곡되어 있습니다 (주로 POSIX 스레드에서 상속 된 역사적 이유로). 조건 변수는 원하는 것에 사용하기에 올바른 기본입니다.
    경우 찾을 std::condition_variable당신은뿐만 아니라 단순히 대신 (약간 느린) Windows 이벤트를 사용할 수 있습니다, 그것으로 편안 너무 이상 또는, 당신은 용기있는 경우, (무서운 낮은 수준의 물건을 포함한다) NtKeyedEvents 주위에 자신의 간단한 이벤트를 구축 할 수 있습니다. DirectX를 사용하면서 어쨌든 이미 Windows에 바인딩되어 있으므로 이식성 손실이 큰 문제는 아닙니다.
  • 고정 된 크기의 작업자 스레드 풀 (하이퍼 스레드 코어를 계산하지 않고 코어 당 하나만)이 실행하는 합리적인 크기의 작업으로 작업을 나눕니다. 마무리 작업이 종속 작업을 대기열에 넣도록하십시오 (무료, 자동 동기화). 각각 수백 가지의 사소한 작업을 수행하는 작업을 수행하십시오 (또는 디스크 읽기와 같은 하나의 긴 블로킹 작업). 캐시 연속 액세스를 선호하십시오.
  • 프로그램 시작시 모든 스레드를 작성하십시오.
  • OS 또는 그래픽 API가 프로그램 수준뿐만 아니라 하드웨어 (PCI 전송, CPU-GPU 병렬 처리, 디스크 DMA 등)에서 더 나은 / 추가 병렬 처리를 위해 제공하는 비동기 기능을 활용하십시오.
  • 내가 언급하지 않은 다른 것 10,000 개.


[1] 그렇습니다. 스케줄러의 속도를 1ms로 낮출 수는 있지만 더 많은 컨텍스트 전환을 유발하고 더 많은 전력을 소비하므로 (더 많은 장치가 모바일 장치 인 세계에서) 눈살을 찌푸립니다. 또한 더 안정적으로 잠을 자지 않기 때문에 해결책이 아닙니다.
[2] 타이머는 스레드의 우선 순위를 높여서 우선 순위가 같은 다른 스레드 중간-중간을 중단하고 우선 RT 동작 인 스케쥴링을 허용합니다. 물론 실제 RT는 아니지만 매우 가깝습니다. 휴면 상태에서 깨어 난다는 것은 스레드가 필요할 때 언제든지 일정을 예약 할 수 있음을 의미합니다.


"높은 수준의 구성 요소 당 하나의 스레드가 없어야"하는 이유를 설명해 주시겠습니까? 두 개의 별도 스레드에서 물리 및 오디오 믹싱이 없어야 함을 의미합니까? 그렇게하지 않을 이유가 없습니다.
Elviss Strazdins

3

업데이트의 FPS와 렌더 둘 다를 60으로 제한하여 달성하고자하는 것이 확실하지 않습니다. 동일한 값으로 제한하면 동일한 스레드에 넣을 수 있습니다.

다른 스레드에서 업데이트와 렌더를 분리 할 때의 목표는 서로 "거의"독립적으로 두어 GPU가 500 FPS를 렌더링하고 업데이트 논리가 여전히 60 FPS가되도록하는 것입니다. 그렇게함으로써 성능이 크게 향상되지는 않습니다.

그러나 당신은 그것이 어떻게 작동하는지 알고 싶다고 말했고 괜찮습니다. C ++에서 뮤텍스는 다른 스레드의 특정 리소스에 대한 액세스를 잠그는 데 사용되는 특수 객체입니다. 즉, mutex를 사용하여 한 번에 하나의 스레드만으로 합리적인 데이터에 액세스 할 수 있도록합니다. 그렇게하려면 아주 간단합니다.

std::mutex mutex;
mutex.lock();
// Do sensible stuff here...
mutex.unlock();

출처 : http://en.cppreference.com/w/cpp/thread/mutex

편집 : 주어진 링크에서와 같이 뮤텍스가 클래스 또는 파일 전체인지 확인하십시오. 그렇지 않으면 각 스레드가 자체 뮤텍스를 생성하므로 아무것도 달성하지 못합니다.

뮤텍스를 잠그는 첫 번째 스레드는 내부 코드에 액세스 할 수 있습니다. 두 번째 스레드가 lock () 함수를 호출하려고 시도하면 첫 번째 스레드가 잠금을 해제 할 때까지 차단됩니다. 따라서 뮤텍스는 while 루프와 달리 blocing 함수입니다. 차단 기능은 CPU에 부담을주지 않습니다.


그리고 블록은 어떻게 작동합니까?
Liuka

두 번째 스레드가 lock ()을 호출하면 첫 번째 스레드가 mutex를 잠금 해제 할 때까지 참을성있게 기다린 후 다음 줄 (이 예에서는 현명한 내용)에서 계속됩니다. 편집 : 두 번째 스레드는 뮤텍스 자체를 잠급니다.
Alexandre Desbiens


1
사용 std::lock_guard하거나 유사하지 .lock()/ .unlock(). RAII는 메모리 관리만을위한 것이 아닙니다!
bcrist
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.