언리얼 렌더링 시스템 분석(02) - 멀티스레드 렌더링
🌐 원문 링크: 剖析虚幻渲染体系(02)- 多线程渲染 (cnblogs.com/timlly)
📅 원문 발행일: 2021-01-25 | ✍️ 저자: Timlly (00 / )💡 시리즈 분류:
Unreal Engine Rendering| 국내 업계 표준 용어 감수 및 수식/도해 복원 적용 완료
보다 원활한 전환을 위해 실제로 UE의 멀티스레드 렌더링 지식에 들어가기 전에 먼저 멀티스레드 프로그래밍의 기본 사항을 배우거나 복습하세요.
2.1.1 멀티스레딩 개요
멀티스레드 프로그래밍이라는 개념은 싱글 코어 시대부터 등장했습니다. 당시 운영체제(예: Windows95)에서는 이미 멀티태스킹 기능을 지원했습니다. 원칙은 각 프로세스의 스레드가 명령을 실행할 기회를 얻을 수 있도록 단일 코어에서 서로 다른 컨텍스트를 전환하는 것입니다.
그러나 2005년에 단일 코어 주 주파수가 4GHz에 도달했을 때 CPU 하드웨어 제조업체인 Intel과 AMD는 속도에도 한계가 있다는 사실을 발견했습니다. 즉, 주 주파수를 높이는 것만으로는 더 이상 시스템의 전체 성능을 크게 향상시킬 수 없습니다.
무어의 단일 코어 컴퓨팅 주파수 법칙이 서서히 끝나자 Intel은 2005년에 처음으로 두 가지 물리적 수준의 스레드 컴퓨팅 장치를 지원하는 Pentium D 및 Pentium IV Extreme Edition 840 시리즈를 출시하는 데 앞장섰습니다. 그로부터 10여년 동안 멀티코어 CPU가 번성했고, AMD가 제조한 라이젠 3990X 프로세서는 이미 64개 코어와 128개 논리 스레드를 탑재하고 있다.

Ryzen 3990X 홍보 포스터에서 눈에 띄게 강조된 코어 및 스레드 수입니다.
하드웨어의 멀티 코어 개발은 소프트웨어에 큰 활용 공간을 제공합니다. 애플리케이션은 멀티 코어 및 멀티 스레드 컴퓨팅 리소스를 최대한 활용할 수 있으며, 멀티 스레드 프로그래밍 모델 및 기술이 다양한 응용 분야에서 등장했습니다. 게임의 엔진으로서 Unreal Engine과 같은 상용 엔진도 멀티스레딩 기술을 활용하여 효율성과 효과를 더욱 완전히 향상시킬 수 있습니다.
다중 스레드 동시성을 사용하는 주요 효과는 두 가지로 요약할 수 있습니다.
**별도의 우려사항입니다.**관련된 코드와 관련되지 않은 코드를 분리하면 프로그램을 더 쉽게 이해하고 테스트할 수 있으므로 오류 가능성이 줄어듭니다. 예를 들어, 게임 엔진은 일반적으로 파일 로딩과 네트워크 전송을 독립된 스레드에 배치하는데, 이는 메인 스레드를 방해하지 않을 뿐만 아니라 논리 코드를 분리하여 더 명확하고 확장 가능하게 만듭니다.
**성능이 향상되었습니다.**더 많은 사람이 더 강력하며, 이 원칙은 CPU에도 적용됩니다(더 많은 코어가 더 강력합니다). 동일한 규모의 작업을 여러 CPU에 분산시켜 동시에 실행할 수 있다면 효율성은 필연적으로 향상될 것입니다.
그러나 CPU 코어 수가 증가할수록 컴퓨터가 얻는 이점은 선형적으로 증가하지 않고 **암달의 법칙(Amdahl's law)**을 따릅니다. 암달의 법칙의 공식은 다음과 같이 정의됩니다.
공식의 각 구성 요소의 의미는 다음과 같습니다.
: 멀티 스레드 처리에서 전체 작업의 이론적인 가속 비율입니다. : 작업의 병렬 부분을 실행하는 데 사용되는 하드웨어 리소스의 스레드 수입니다. : 병렬로 처리할 수 있는 작업의 비율입니다.
구체적인 예를 들자면, 8개의 코어와 16개의 스레드가 있는 CPU가 특정 작업을 처리하는 데 사용되고 이 작업의 70%가 병렬로 처리될 수 있다고 가정하면 이론적인 가속 비율은 다음과 같습니다.
멀티스레드 프로그래밍이 가져오는 이점은 코어 수에 정비례하지 않는다는 것을 알 수 있습니다. 실제로 그 곡선은 다음과 같습니다.

암달의 법칙에 의해 밝혀진 코어 수 및 속도 향상 비율의 그림입니다. 병렬 작업의 비율이 낮을수록 가속 효과가 더 나쁘다는 것을 알 수 있습니다. 병렬 작업의 비율이 50%이면 기본적으로 16개의 코어가 가속 한도에 도달하며 나중에 몇 개의 코어를 추가하더라도 도움이 되지 않습니다. 병렬 작업 비율이 95%인 경우 가속 한도는 2048개 코어까지 도달하지 않습니다.
암달의 법칙은 우리에게 잔인한 현실을 가져오지만, 작업 병렬성 비율을 100%에 가깝게 늘릴 수 있다면 속도 향상 한도는 크게 향상될 수 있습니다.
위 공식에서 볼 수 있듯이
구체적인 예를 들자면, 언리얼 엔진 프로젝트 소스 코드나 셰이더를 컴파일할 때 기본적으로 100% 병렬이기 때문에 이론적으로 거의 선형에 가까운 가속 비율을 얻을 수 있어 멀티 코어 시스템에서 컴파일 시간을 대폭 단축할 수 있습니다.
성능을 향상시키기 위해 다중 스레드 동시성을 사용하는 방법에는 두 가지가 있습니다.
**작업 병렬성.**단일 작업을 여러 부분으로 나누고 병렬로 실행하여 총 실행 시간을 줄입니다. 이 방법은 간단하고 직관적인 것처럼 보이지만 실제로는 여러 부분 사이에 종속성이 있을 수 있으므로 복잡할 수 있습니다.
**데이터 병렬성.**작업 병렬성이란 알고리즘(명령 실행) 부분을 의미합니다. 즉, 각 스레드에서 실행되는 명령이 다릅니다. 데이터 병렬성은 동일한 명령을 나타내지만 실행되는 데이터는 다릅니다. SIMD는 데이터 병렬 처리 방법이기도 합니다.
멀티스레드 동시성의 이점은 위에 설명되어 있으며 다음에는 부작용에 대해 설명하겠습니다. 요약하면 부작용은 다음과 같습니다.
**데이터 경쟁을 유발합니다.**멀티 스레드 액세스는 종종 동일한 코드 조각을 교차 실행하거나 동일한 리소스를 작동하거나 멀티 코어 CPU에서 높은 캐시 동기화 문제가 있습니다. 이러한 변경으로 인해 다양한 데이터 동기화 오류나 데이터 읽기 및 쓰기 오류가 발생하여 다양한 비정상적인 결과가 발생합니다. 이것이 데이터 경쟁이다.
**로직이 복잡하고 디버그하기 어렵습니다.**멀티스레딩의 동시성 방식은 독특하지 않고 예측 불가능하기 때문에 데이터 경쟁을 피하기 위해 복잡하고 다양한 동기화 작업이 추가되는 경우가 많습니다. 또한 코드는 분리되고, 단편화되고, 번거롭고, 이해하기 어려워집니다. 코드 지원을 추가하면 후속 유지 관리 및 확장에 헤아릴 수 없는 장애물이 발생합니다. 또한 확률이 낮은 이벤트로 인해 재현하기 어려운 BUG가 발생하여 디버깅 및 문제 해결이 훨씬 더 어려워질 수 있습니다.
**반드시 효율성이 향상되는 것은 아닙니다.**멀티스레딩 기술을 사용하면 실제로 효율성이 향상되지만 절대적이지는 않습니다. 이는 물리적 코어, 동기화 메커니즘, 런타임 상태, 동시성 비율 등과 같은 요소와 관련이 있는 경우가 많습니다. 극단적인 경우 또는 적절하게 사용되지 않으면 실제로 프로그램 효율성이 저하될 수 있습니다.
2.1.2 멀티스레딩 개념
이 섹션에서는 멀티스레드 프로그래밍 기술과 관련된 기본 개념을 설명합니다.
- 프로세스
프로세스는 운영체제가 애플리케이션을 실행하는 기본 단위이자 실체입니다. 그 자체는 일반적으로 커널 개체, 주소 공간, 통계 정보 및 여러 스레드를 포함하는 컨테이너일 뿐입니다. 실제로 코드 명령 자체를 실행하지는 않지만 실행을 위해 프로세스 내의 스레드로 전달됩니다.
Windows의 경우 운영 체제는 프로세스를 생성할 때 이에 대한 스레드도 생성하는데, 이를 메인 스레드(기본 스레드, 메인 스레드)라고 합니다.
Unix의 경우 프로세스와 메인 스레드는 실제로 동일합니다. 운영 체제는 스레드의 존재를 알지 못합니다. 스레드는 경량 프로세스의 개념에 더 가깝습니다.
프로세스에는 우선순위라는 개념이 있습니다. Windows에서는 낮음부터 높음 순으로 낮음, 보통 이하, 보통, 보통 이상, 높음 및 실시간입니다. (아래 그림 참조)

기본적으로 프로세스의 우선순위는 보통입니다. 우선순위가 높은 프로세스는 실행 기회와 시간을 먼저 얻습니다.
- 스레드
스레드는 코드를 실행할 수 있는 개체입니다. 일반적으로 독립적으로 존재할 수 없으며 프로세스에 연결되어야 합니다. 프로세스에는 프로세스의 데이터를 공유하여 여러 작업을 병렬 또는 동시에 수행할 수 있는 여러 스레드가 있을 수 있습니다.
단일 코어 CPU에서 운영 체제(예: Windows)는 예약을 위해 라운드 로빈 방법을 사용하여 여러 스레드가 동시에 실행되는 것처럼 보이게 할 수 있습니다. (아래 사진)

멀티 코어 CPU에서는 병렬 처리를 달성하기 위해 스레드가 서로 다른 CPU 코어에서 동시에 실행되도록 예약할 수 있습니다.

멀티 코어 CPU에서 SMP를 사용하는 Windows의 실행 다이어그램. 처리를 기다리는 스레드는 다른 CPU 코어에 예약됩니다.
각 스레드는 자체 실행 명령 컨텍스트(예: Windows의 IP(명령어 레지스터 주소) 및 SP(스택 시작 레지스터 주소)), 실행 스택 및 TLS(Thread Local Storage, 스레드 로컬 캐시)를 가질 수 있습니다.

Windows 스레드 생성 및 초기화 다이어그램.
Thread Local Storage는 저장 기간입니다. 객체의 생명주기는 스레드의 생명주기와 동일합니다. 스레드가 시작될 때 할당되고 스레드가 끝날 때 재활용됩니다. 각 스레드에는 개체의 자체 인스턴스가 있으며 이러한 개체에 액세스하고 수정해도 경쟁 조건이 발생하지 않습니다.
스레드에도 우선순위라는 개념이 있습니다. 우선순위가 높을수록 명령을 실행할 수 있는 첫 번째 기회가 더 높은 우선순위에 부여됩니다.
스레드 상태에는 일반적으로 실행 중 상태, 일시 중지 상태 등이 포함됩니다. Windows는 다음 인터페이스를 사용하여 스레드 상태를 전환할 수 있습니다.
// 스레드(Thread)
DWORD SuspendThread(HANDLE hThread);
// 스레드(Thread)
DWORD ResumeThread(HANDLE hThread);동일한 스레드가 여러 번 일시 중단될 수 있습니다. 실행 상태를 재개하려면 계속 실행 인터페이스를 동일한 횟수만큼 호출해야 합니다.
- 코루틴
코루틴은 일반적으로 동일한 스레드에서 실행되고 동일한 스레드의 다른 시간 세그먼트를 사용하여 스레드 오버헤드, 프로세스 전환 및 예약 없이 명령을 실행하는 경량 사용자 모드 스레드입니다. 사용자 관점에서 코루틴 메커니즘은 동일한 스레드에서 비동기 작업과 코딩 방법을 시뮬레이션하는 데 사용될 수 있습니다. 동일한 스레드 내에서는 데이터 경쟁이 발생하지 않지만 스레드 차단으로 인해 차단되기도 합니다.
- 섬유질
Fiber는 코루틴과 마찬가지로 애플리케이션이 자체 스레드의 작동 방식을 독립적으로 결정할 수 있는 경량 사용자 모드 스레드이기도 합니다. 운영 체제 커널은 파이버가 존재하는지 모르고 이를 예약하지도 않습니다.
- 경합 조건
동일한 프로세스는 다중 스레드를 허용하며 이러한 스레드는 프로세스의 주소 공간, 데이터 구조 및 컨텍스트를 공유할 수 있습니다. 프로세스의 동일한 데이터 블록에는 짧은 시간 내에 동시에 읽고 쓰는 여러 스레드가 있을 수 있으며, 이로 인해 데이터 이상이 발생하고 예측할 수 없는 결과가 발생할 수 있습니다. 이러한 예측 불가능성은 경쟁 조건을 만듭니다.
원자 연산, 임계 섹션, 읽기-쓰기 잠금, 커널 개체, 세마포어, 뮤텍스, 펜스, 장벽, 이벤트 등과 같은 경쟁 조건을 피하기 위한 많은 기술이 있습니다.
- 병렬성
두 개 이상의 스레드가 동시에 작업을 수행하는 메커니즘입니다. 일반적으로 여러 코어와 여러 물리적 스레드가 동시에 실행되는 CPU의 동작을 병렬성이라고 할 수 있지만 단일 코어의 멀티스레딩을 병렬성이라고 할 수는 없습니다.
- 동시성
작업을 수행하기 위해 시간 조각을 사용하는 최소 두 개의 스레드 메커니즘은 병렬 처리의 보다 일반적인 형태입니다. 단일 코어 CPU에서 동시에 실행되는 여러 스레드도 동시성이라고 할 수 있습니다.

동시성의 두 가지 형태 - 상단: 듀얼 물리적 코어의 동시 실행(병렬); 하단: 단일 코어의 다중 작업 전환(동시성)
실제로 멀티코어 프로세서에서는 동시성과 병렬성이 공존할 수 있습니다. 예를 들어, 아래 그림과 같이 듀얼 코어가 있고 각 코어는 동시에 여러 작업을 전환합니다.

일부 참고 자료에서는 병렬성과 동시성을 엄격하게 구분하지만 일부 참고 자료에서는 차이점을 명시적으로 지적하지 않습니다. 병렬성과 동시성의 개념은 Unreal Engine의 멀티스레드 렌더링 아키텍처와 API에 자주 나타나므로 Unreal은 이 둘을 명확하게 구분합니다.
- 스레드 풀
스레드 풀은 작업 동시성의 새로운 방법을 제공합니다. 호출자는 일련의 병렬 작업과 그룹화 전략만 전달하면 스레드 풀의 여러 스레드를 사용하여 작업을 동시에 실행할 수 있습니다. 이를 통해 호출자가 스레드의 호출 및 관리 세부 정보에 직접 연결할 필요가 없으며 호출자의 비용이 절감되고 스레드 예약 효율성과 처리량이 향상됩니다.
그러나 스레드 풀을 생성할 때 사용할 수 있는 스레드 수, 효율적인 작업 할당, 작업이 완료될 때까지 기다려야 하는지 여부 등 몇 가지 주요 설계 문제가 동시성 효율성에 영향을 미칩니다.
스레드 풀은 사용자 정의 방식으로 구현되거나 C++, 운영 체제 또는 타사 라이브러리에서 제공되는 API를 직접 사용할 수 있습니다.
2.1.3 C++의 멀티스레딩
C++11 이전에는 C++의 멀티스레딩 지원이 기본적으로 0이었고, 휘발성 같은 몇 가지 쓸모없는 키워드만 제공되었습니다. 멀티스레딩이 C++ 표준에 완전히 통합된 것은 C++11 표준이 되어서야 가능했으며, 사용자가 크로스 플랫폼 멀티스레드 호출을 쉽게 구현할 수 있도록 관련 키워드와 STL 표준 라이브러리가 제공되었습니다.
물론 사용자의 경우 C++11 스레드 라이브러리를 사용하여 멀티스레딩을 구현할 수도 있고, 특정 시스템 플랫폼에서 제공하는 멀티스레딩 API에 따라 스레드 라이브러리를 사용자 정의하거나 ACE 및 Boost::thread와 같은 타사 라이브러리를 사용할 수도 있습니다. C++와 함께 제공되는 멀티스레딩 라이브러리를 사용하면 몇 가지 장점이 있습니다. 첫째, 사용이 간단하고 편리하며 종속성이 거의 없습니다. 둘째, 크로스 플랫폼이므로 시스템의 하위 계층에 주의를 기울일 필요가 없습니다.
2.1.3.1 C++ 멀티스레딩 키워드
- 스레드_로컬
thread_local은 C++에서 스레드 로컬 저장소를 구현하는 핵심입니다. 이 키워드가 추가된 변수는 각 스레드가 자체 데이터 복사본을 가지며 데이터 경쟁을 피하기 위해 동일한 데이터를 공유하지 않음을 의미합니다.
C11 키워드 _Thread_local은 스레드 지역 변수를 정의하는 데 사용됩니다. 헤더파일 <threads.h>에는 위 키워드의 동의어로 thread_local이 정의되어 있다. 예를 들어:
#include <threads.h>
thread_local int foo = 0;C++11에 도입된 thread_local 키워드는 다음과 같은 상황에서 사용됩니다.
네임스페이스(전역) 변수.
정적 변수를 파일로 저장합니다.
함수 정적 변수.
정적 멤버 변수.
또한 다양한 컴파일러는 스레드 지역 변수를 선언하는 고유한 방법을 제공합니다.
// Visual C++, Intel C/C++ (Windows systems), C++Builder, Digital Mars C++
__declspec(thread) int number;
// Solaris Studio C/C++, IBM XL C/C++, GNU C, Clang, Intel C++ Compiler (Linux systems)
__thread int number;
// C++ Builder
int __thread number;- 휘발성
휘발성 수정자를 사용하는 변수는 메모리의 값이 언제든지 변경될 수 있음을 의미합니다. 또한 컴파일러에게 최적화를 수행할 수 없음을 알려줍니다. 이 변수의 값을 사용할 때마다 메모리에서 읽어야 하며, 레지스터의 값을 직접 사용해서는 안 됩니다.
구체적인 예를 들어 보겠습니다. 다음과 같은 코드 조각이 있다고 가정해 보겠습니다.
int a = 10;
volatile int *p = &a;
int b, c;
b = *p;
c = *p;p가 휘발성으로 수정되지 않은 경우 b = *p 및 c =*p는 메모리에서 p 값을 한 번만 가져오면 되며 b와 c의 값은 10이어야 합니다.
휘발성의 영향을 고려하면 b = *p 문을 실행한 후 p 값이 다른 스레드에 의해 수정된다고 가정하고 c =*p를 실행하면 메모리에서 p 값을 다시 읽습니다. 이때 c의 값은 더 이상 10이 아닌 새로운 값이다.
그러나 휘발성은 다중 스레드의 동기화 문제를 해결할 수 없으며 다음 세 가지 상황에만 적합합니다.
신호 처리기와 관련된 경우.
메모리 매핑된 하드웨어와 관련된 경우.
비로컬 점프(setjmp 및 longjmp)와 관련된 경우.
- 표준::원자
엄밀히 말하면 원자는 키워드가 아니라 특정 유형의 원자 연산을 지원할 수 있는 STL의 템플릿 클래스입니다.
원자 유형을 사용한다는 것은 이 유형 인스턴스의 읽기 및 쓰기 작업이 원자적이고 다른 스레드에 의해 절단될 수 없음을 의미하므로 스레드 안전 및 동기화 목표를 달성합니다.
일부 독자들은 기본 유형의 연산에도 왜 원자 연산이 필요한지 궁금해할 수 있습니다. 예를 들어:
int cnt = 0;
auto f = [&]{cnt++;};
std::thread t1{f}, t2{f}, t3{f};위의 세 스레드는 동시에 함수 f를 호출합니다. 이 함수는 cnt++만 실행합니다. C++ 차원에서는 실행문이 하나만 있는 것 같습니다. 이론적으로는 동기화 문제가 없어야 합니다. 그러나 어셈블리 명령어로 컴파일한 후에는 여러 명령어가 발생하므로 다중 스레드에서 스레드 컨텍스트 전환이 발생하고 예측할 수 없는 동작이 발생합니다.
이러한 상황을 방지하려면 원자 유형을 추가해야 합니다.
std::atomic<int> cnt{0}; // cnt추가(Add)원자적 。
auto f = [&]{cnt++;};
std::thread t1{f}, t2{f}, t3{f};Atomic을 추가한 후에는 모든 스레드의 실행 결과가 확실해지며 변수를 정상적으로 계산할 수 있습니다. Atomic의 구현 메커니즘은 임계 섹션의 구현 메커니즘과 유사하지만 효율성 면에서 임계 섹션보다 빠릅니다.
C++의 단일 명령문이 여러 어셈블리 명령어를 생성할 수 있다는 점을 자세히 설명하기 위해 컴파일러 탐색기를 사용하여 C++ 어셈블된 명령어를 실시간으로 탐색할 수 있습니다.

컴파일러 탐색기는 왼쪽의 C++ 문의 어셈블리 명령을 동적으로 컴파일합니다. 위에 표시된 C++ 코드에는 컴파일 후 일대다 어셈블리 명령이 있을 수 있으며 이는 원자 연산의 필요성을 확인합니다.
std::atomic의 기능과 인터페이스를 최대한 활용함으로써 많은 비차단 및 잠금 프리 스레드 안전 데이터 구조와 알고리즘을 구현할 수 있습니다. 이 점에 대해 자세히 알아보려면 "C++ 동시성 실행"을 강력히 권장합니다.
2.1.3.2 C++ 스레드
C++의 스레드 유형은 std::thread이며 제공되는 인터페이스는 다음과 같습니다.
인터페이스 분석하다
가입하다 메인 스레드를 조인하여 스레드 실행이 완료될 때까지 메인 스레드가 기다리도록 합니다.
분리하다 메인 스레드가 스레드 실행이 완료될 때까지 기다릴 필요가 없도록 메인 스레드에서 분리됩니다.
교환 스레드 개체를 다른 스레드와 교환합니다.
결합 가능 메인 스레드에 조인할 수 있는지 쿼리합니다.
get_id 이 스레드의 고유 식별자를 가져옵니다.
Native_handle 구현 계층의 스레드 핸들을 반환합니다.
하드웨어 동시성 정적 인터페이스는 하드웨어가 지원하는 동시 스레드 수를 반환합니다.
사용 예:
#include <iostream>
#include <thread>
#include <chrono>
void foo()
{
// simulate expensive operation
std::this_thread::sleep_for(std::chrono::seconds(1));
}
int main()
{
std::cout << "starting thread...\n";
std::thread t(foo); // 스레드(Thread)객체/오브젝트 ,실행(Execute)의 함수 。
std::cout << "waiting for thread to finish..." << std::endl;
t.join(); // 추가(Add)스레드(Thread),스레드(Thread)필수: 스레드(Thread)실행(Execute)。
std::cout << "done!\n";
}산출:
starting thread...
waiting for thread to finish...
done!호출 스레드와 새 스레드 간에 데이터를 동기화해야 하는 경우 C++의 std::promise 및 std::future와 같은 메커니즘을 사용할 수 있습니다. 샘플 코드:
#include <vector>
#include <thread>
#include <future>
#include <numeric>
#include <iostream>
void accumulate(std::vector<int>::iterator first,
std::vector<int>::iterator last,
std::promise<int> accumulate_promise)
{
int sum = std::accumulate(first, last, 0);
accumulate_promise.set_value(sum); // Notify future
}
int main()
{
// Demonstrate using promise<int> to transmit a result between threads.
std::vector<int> numbers = { 1, 2, 3, 4, 5, 6 };
std::promise<int> accumulate_promise;
std::future<int> accumulate_future = accumulate_promise.get_future();
std::thread work_thread(accumulate, numbers.begin(), numbers.end(),
std::move(accumulate_promise));
// future::get() will wait until the future has a valid result and retrieves it.
// Calling wait() before get() is not needed
//accumulate_future.wait(); // wait for result
std::cout << "result = " << accumulate_future.get() << '\n';
work_thread.join(); // wait for thread completion
}출력 결과:
result = 21그러나 std::thread의 실행은 비동기식으로 보장되지 않으며 현재 스레드에서도 실행될 수 있습니다.
비동기를 강제로 적용해야 하는 경우 std::async를 사용할 수 있습니다. std::launch::async 및 std::launch::deferred라는 두 가지 비동기 메서드를 지정할 수 있습니다. 전자는 새 스레드를 사용하여 작업을 비동기적으로 실행하는 것을 의미하고, 후자는 현재 스레드에서 작업을 실행하므로 지연된다는 의미입니다. 사용 예:
#include <iostream>
#include <vector>
#include <algorithm>
#include <numeric>
#include <future>
#include <string>
#include <mutex>
std::mutex m;
struct X {
void foo(int i, const std::string& str) {
std::lock_guard<std::mutex> lk(m);
std::cout << str << ' ' << i << '\n';
}
void bar(const std::string& str) {
std::lock_guard<std::mutex> lk(m);
std::cout << str << '\n';
}
int operator()(int i) {
std::lock_guard<std::mutex> lk(m);
std::cout << i << '\n';
return i + 10;
}
};
template <typename RandomIt>
int parallel_sum(RandomIt beg, RandomIt end)
{
auto len = end - beg;
if (len < 1000)
return std::accumulate(beg, end, 0);
RandomIt mid = beg + len/2;
auto handle = std::async(std::launch::async,
parallel_sum<RandomIt>, mid, end);
int sum = parallel_sum(beg, mid);
return sum + handle.get();
}
int main()
{
std::vector<int> v(10000, 1);
std::cout << "The sum is " << parallel_sum(v.begin(), v.end()) << '\n';
X x;
// Calls (&x)->foo(42, "Hello") with default policy:
// may print "Hello 42" concurrently or defer execution
auto a1 = std::async(&X::foo, &x, 42, "Hello");
// Calls x.bar("world!") with deferred policy
// prints "world!" when a2.get() or a2.wait() is called
auto a2 = std::async(std::launch::deferred, &X::bar, x, "world!");
// Calls X()(43); with async policy
// prints "43" concurrently
auto a3 = std::async(std::launch::async, X(), 43);
a2.wait(); // prints "world!"
std::cout << a3.get() << '\n'; // prints "53"
} // if a1 is not done at this point, destructor of a1 prints "Hello 42" here실행 결과:
The sum is 10000
43
Hello 42
world!
53게다가 C++20은 이미 경량 코루틴을 지원합니다. 관련 키워드: co_await, co_return, co_yield. 이는 C# 등 스크립트 언어의 개념 및 사용법과 완전히 동일하지만 동작 및 구현 메커니즘이 약간 다를 수 있습니다. 이 기사에서는 이에 대해 더 이상 논의하지 않습니다.
2.1.3.3 C++ 다중 스레드 동기화
스레드 동기화에는 다양한 메커니즘이 있습니다. C++에서는 다음을 지원합니다.
- 표준::원자
[2.1.3.1 C++ 멀티스레드 키워드](#2.1.3.1 C++ 멀티스레드 키워드)에서는 std::atomic에 대한 상세한 분석을 수행하여 다중 스레드 간 공유 데이터의 데이터 레이싱 문제를 방지할 수 있습니다. 또한 원자 데이터를 보다 정확하고 효율적으로 동기화하기 위해 다양한 인터페이스와 상태 쿼리도 제공합니다. 일반적인 인터페이스 및 분석은 다음과 같습니다.
인터페이스 이름 분석하다
is_lock_free 원자 개체에 잠금이 없는지 확인합니다.
저장 원자 객체에 값을 저장합니다.
부하 원자 개체에서 값을 로드합니다.
교환 원자 개체의 값을 가져와서 지정된 값으로 바꿉니다.
비교_교환_약함, 비교_교환_강함 원자 객체의 값을 예상 값(예상)과 비교합니다. 동일하다면 목표 값(원하는)으로 바꾸고 true를 반환합니다. 서로 다른 경우 원자 객체의 값을 예상 값(예상)에 로드하고 false를 반환합니다. 약한 모드는 호출 스레드를 차단하지 않으며, 강한 모드는 원자 개체의 값이 예상 값과 동일할 때까지 호출 스레드를 차단합니다.
fetch_add, fetch_sub, fetch_and, fetch_or, fetch_xor 원자 객체의 값을 가져와서 덧셈, 뺄셈과 같은 연산을 수행합니다.
연산자 ++, 연산자 --, 연산자 +=, 연산자 -=, ... 원자 객체에 대한 다양한 연산자에 응답하며, 연산자의 의미는 일반 변수의 의미와 동일합니다.
또한 C++20은 wait, inform_one, inform_all 및 기타 동기화 인터페이스도 지원합니다.
스레드로부터 안전한 비차단 데이터 구조는 Compare_exchange_weak 인터페이스를 사용하여 쉽게 구현할 수 있습니다. 예:
#include <atomic>
#include <future>
#include <iostream>
template<typename T>
struct node
{
T data;
node* next;
node(const T& data) : data(data), next(nullptr) {}
};
template<typename T>
class stack
{
public:
std::atomic<node<T>*> head; // 힙 , 을(를) 활용하여 원자적 .
public:
//
void push(const T& data)
{
node<T>* new_node = new node<T>(data);
// 의 로써 포인트 의 포인트 .
new_node->next = head.load(std::memory_order_relaxed);
// 의 포인트 및 의 포인트 대비 , 만약 new_node->next==head, 스레드(Thread)수정 head, 로써 headnew_node, 로부터 push.
// , 만약 new_node->next!=head, 스레드(Thread)수정 head, 스레드(Thread)수정 의 head저장(Save)까지 new_node->next, 루프 검사/감지(Detect).
while(!head.compare_exchange_weak(new_node->next, new_node,
std::memory_order_release,
std::memory_order_relaxed))
; // 루프
}
};
int main()
{
stack<int> s;
auto r1 = std::async(std::launch::async, &stack<int>::push, &s, 1);
auto r2 = std::async(std::launch::async, &stack<int>::push, &s, 2);
auto r3 = std::async(std::launch::async, &stack<int>::push, &s, 3);
r1.wait();
r2.wait();
r3.wait();
// print the stack's values
node<int>* node = s.head.load(std::memory_order_relaxed);
while(node)
{
std::cout << node->data << " ";
node = node->next;
}
}산출:
2 3 1멀티 스레드 동기화는 Atom과 해당 인터페이스를 사용하여 쉽게 수행할 수 있으며, 멀티 스레드는 비동기적으로 스택에 들어가기 때문에 스택의 요소가 코딩 순서와 반드시 일치하지는 않는다는 것을 알 수 있습니다.
위의 코드에는 메모리 액세스 시퀀스 표시도 포함됩니다.
순차적으로 일관된 순서.
획득-해제 시퀀스(memory_order_consume, memory_order_acquire, memory_order_release 및 memory_order_acq_rel).
자유 시퀀스(memory_order_relaxed).
이에 대한 자세한 내용은 첫 번째 기사의 메모리 장벽을 참조하거나 "C++ 동시성 실행"의 5.3장 동기 작업 및 강제 정렬을 참조하세요.
- 표준::뮤텍스
std::mutex는 뮤텍스입니다. 동시에 하나의 스레드에서만 코드 조각에 액세스할 수 있도록 해당 범위 내에서 중요 섹션에 들어가게 됩니다. 다른 스레드가 조각을 실행하려고 하면 차단됩니다. std::mutex는 종종 std::lock_guard와 함께 사용됩니다. 샘플 코드는 다음과 같습니다.
#include <iostream>
#include <map>
#include <string>
#include <chrono>
#include <thread>
#include <mutex>
std::map<std::string, std::string> g_pages;
std::mutex g_pages_mutex; // 선언 뮤텍스
void save_page(const std::string &url)
{
// simulate a long page fetch
std::this_thread::sleep_for(std::chrono::seconds(2));
std::string result = "fake content";
// std::lock_guard을(를) 활용하여 , 로써 및 및 해제(Release)뮤텍스 .
std::lock_guard<std::mutex> guard(g_pages_mutex);
g_pages[url] = result;
}
int main()
{
std::thread t1(save_page, "http://foo");
std::thread t2(save_page, "http://bar");
t1.join();
t2.join();
// safe to access g_pages without lock now, as the threads are joined
for (const auto &pair : g_pages) {
std::cout << pair.first << " => " << pair.second << '\n';
}
}산출:
http://bar => fake content
http://foo => fake content또한 std::mutex의 잠금 및 잠금 해제를 수동으로 작동하면 특정 표시를 기다리는 것과 같은 몇 가지 특별한 동작을 수행할 수 있습니다.
#include <chrono>
#include <thread>
#include <mutex>
bool flag;
std::mutex m;
void wait_for_flag()
{
std::unique_lock<std::mutex> lk(m); // 을(를) 활용하여 std::unique_lockstd::lock_guard. std::unique_lock로써 구현 , 만약 현재(Current)로써 및 스레드(Thread), 이면 레이턴시 까지 스레드(Thread)해제(Release), .
while(!flag)
{
lk.unlock(); // 뮤텍스
std::this_thread::sleep_for(std::chrono::milliseconds(100)); // 100ms,,스레드(Thread)로써 뮤텍스 ,로써 flag플래그 。
lk.lock(); // 뮤텍스
}
}- 표준::조건_변수
std::condition_variable 및 std::condition_variable_any는 둘 다 조건 변수이자 C++ 표준 라이브러리의 구현입니다. 둘 다 뮤텍스와 함께 사용해야 합니다. std::condition_variable_any가 더 일반적이므로 성능에 더 많은 오버헤드가 발생합니다. 따라서 먼저 std::condition_variable 사용을 고려해야 합니다.
조건변수 인터페이스와 뮤텍스 활용을 통해 스레드 간 대기, 알림 등의 작업을 쉽게 수행할 수 있습니다. 예:
#include <iostream>
#include <string>
#include <thread>
#include <mutex>
#include <condition_variable>
std::mutex m;
std::condition_variable cv; // 선언 조건 변수
std::string data;
bool ready = false;
bool processed = false;
void worker_thread()
{
// 까지 스레드(Thread)ready로 true.
std::unique_lock<std::mutex> lk(m);
cv.wait(lk, []{return ready;});
// 뮤텍스 의
std::cout << "Worker thread is processing data\n";
data += " after processing";
// 데이터 스레드(Thread)
processed = true;
std::cout << "Worker thread signals data processing completed\n";
// , 로써 스레드(Thread).
lk.unlock();
cv.notify_one();
}
int main()
{
std::thread worker(worker_thread);
data = "Example data";
// send data to the worker thread
{
std::lock_guard<std::mutex> lk(m);
ready = true;
std::cout << "main() signals data ready for processing\n";
}
cv.notify_one();
// wait for the worker
{
std::unique_lock<std::mutex> lk(m);
cv.wait(lk, []{return processed;});
}
std::cout << "Back in main(), data = " << data << '\n';
worker.join();
}산출:
main() signals data ready for processing
Worker thread is processing data
Worker thread signals data processing completed
Back in main(), data = Example data after processing- 표준::미래
C++의 미래(기대)는 미래의 반환 값에 액세스할 수 있는 메커니즘이며 다중 스레드 동기화에 자주 사용됩니다. future를 생성할 수 있는 유형은 std::async, std::packaged_task, std::promise입니다.
future 객체는 이벤트 대기 및 동기화를 달성하기 위해 wait, wait_for 및 wait_until을 실행할 수 있습니다. 샘플 코드:
#include <iostream>
#include <future>
#include <thread>
int main()
{
// 로부터 packaged_task페치/가져오기(Fetch)의 future
std::packaged_task<int()> task([]{ return 7; }); // wrap the function
std::future<int> f1 = task.get_future(); // get a future
std::thread t(std::move(task)); // launch on a thread
// 로부터 async()페치/가져오기(Fetch)의 future
std::future<int> f2 = std::async(std::launch::async, []{ return 8; });
// 로부터 promise페치/가져오기(Fetch)의 future
std::promise<int> p;
std::future<int> f3 = p.get_future();
std::thread( [&p]{ p.set_value_at_thread_exit(9); }).detach();
// future
std::cout << "Waiting..." << std::flush;
f1.wait();
f2.wait();
f3.wait();
std::cout << "Done!\nResults are: " << f1.get() << ' ' << f2.get() << ' ' << f3.get() << '\n';
t.join();
}산출:
Waiting...Done!
Results are: 7 8 92.1.4 멀티스레딩 구현 메커니즘
멀티스레딩은 병렬 내용에 따라 데이터 병렬성과 작업 병렬성의 두 가지 유형으로 나눌 수 있습니다. 데이터 병렬성은 서로 다른 스레드가 동일한 논리를 실행하기 위해 서로 다른 데이터를 전달한다는 것을 의미합니다. 가장 일반적인 데이터 병렬 애플리케이션은 MMX 명령, SIMD 기술, 컴퓨팅 셰이더 등입니다. 작업 병렬성이란 서로 다른 스레드가 서로 다른 논리를 실행하며 데이터가 동일하거나 다를 수 있음을 의미합니다. 예를 들어, 게임 엔진은 서로 다른 작업을 병렬로 수행할 수 있도록 파일 로딩, 오디오 처리, 네트워크 수신, 물리 시뮬레이션까지 별도의 스레드에 배치하는 경우가 많습니다.
멀티스레딩을 세분성(granularity)과 방법에 따라 나누면 선형 분할, 재귀적 분할, 작업 유형 분할 등이 있습니다.
선형 분할 방법의 가장 간단한 적용은 연속 배열의 요소를 여러 부분으로 나누고 데이터의 각 부분을 UE의 병렬화된 std::for_each 및 ParallelFor와 같은 실행을 위해 스레드에 전달하는 것입니다.

선형 분할 다이어그램. 연속 데이터는 여러 부분으로 나뉘어 병렬 실행을 위해 여러 스레드로 전달됩니다.
선형으로 분할된 병렬 실행이 끝나면 일반적으로 호출 스레드가 병렬 결과를 병합하고 동기화해야 합니다.
재귀적 분할 방법은 연속된 데이터를 특정 규칙에 따라 여러 부분으로 나누고, 특정 규칙에 따라 분할이 중지될 때까지 각 부분을 더 세분화된 부분으로 나눌 수 있습니다. 빠른 정렬에 자주 사용됩니다.
빠른 정렬에는 두 가지 기본 단계가 있습니다. 즉, 피벗 요소 앞이나 뒤의 데이터를 나눈 다음 피벗 요소 앞과 뒤의 배열 절반을 다시 빠르게 정렬하는 것입니다. 어떤 항목이 피벗 요소 앞과 뒤에 있는지는 정렬이 완료된 후에만 알 수 있으므로 데이터를 단순(선형) 분할하는 것만으로는 병렬성을 달성할 수 없습니다. 이 알고리즘을 병렬화할 때 재귀를 사용하는 것을 생각하는 것은 당연합니다. 각 재귀 수준에서는 피벗 요소 앞뒤에 어떤 요소가 오는지 알아야 하기 때문에 Quick_sort 함수를 여러 번 호출합니다.

재귀적 분할 방법의 개략도.
대규모 프레임워크 내의 논리는 일반적으로 서로 독립적이거나 특정 종속성을 가질 수 있는 여러 하위 작업으로 나뉩니다. 각 작업은 실행을 위해 스레드로 전달됩니다. 즉, 스레드는 완전히 독립적이며 각 스레드는 자신이 수행해야 하는 작업에만 집중하면 됩니다.

업무분할의 개략도.
하위 작업을 합리적으로 배치하고 분할하여 종속성을 줄이고 하위 작업 간의 동기화를 기다리는 것은 작업 효율성을 높이는 데 유용한 무기입니다. 그러나 이를 달성하려면 신중한 설계와 구현은 물론 반복적인 디버깅과 수정이 필요한 경우가 많습니다.
위의 구현 메커니즘을 Fork-Join(fork-merge) 병렬 모델이라고도 합니다. 작동 메커니즘과 직렬 모델의 비교는 다음과 같습니다.

상단: 직렬 실행 모델; 하단: Fork-Join 병렬 실행 모델.
GDC2010의 연설 작업 기반 멀티스레딩 - 100개 코어를 프로그래밍하는 방법에서는 작업 기반 멀티스레딩 실행 메커니즘을 채택하는 방법을 자세히 설명했습니다.

작업 기반 멀티스레딩은 스레드 기반 아키텍처보다 훨씬 우수하며 멀티코어의 장점을 최대한 활용하여 각 코어를 계속 사용하게 할 수 있습니다.

또한 이 기사에서는 정렬 및 미로 길찾기와 같은 실제 사례에 작업 기반 멀티스레딩을 적용하는 방법에 대해서도 언급합니다.
2.2 최신 그래픽 API의 멀티스레딩 기능
2.2.1 기존 그래픽 API의 멀티스레딩 기능
OpenGL 및 DirectX10 이전 버전의 그래픽 API에서 모든 그리기 명령은 선형 및 차단 방식입니다. 즉, Draw 인터페이스가 호출될 때마다 즉시 반환되지 않고 호출 스레드가 중단됩니다. 이러한 CPU와 GPU 간의 상호 작용 메커니즘은 싱글 코어 시대에는 성능에 덜 눈에 띄는 영향을 미쳤지만, 멀티 코어 시대가 도래하면서 이러한 상호 작용 메커니즘은 분명히 실행 성능에 심각한 영향을 미칠 것입니다.
게임 엔진의 렌더러가 여전히 단일 스레드인 경우 CPU 성능 병목 현상이 발생하여 성능을 향상하거나 시각적 콘텐츠를 풍부하게 하기 위해 멀티 코어 컴퓨팅 리소스를 사용할 수 없게 되는 경우가 많습니다.

그리기 명령의 기존 그래픽 API 선형 실행의 개략도.
단일 스레드 렌더러는 종종 단일 CPU 코어가 전체 용량으로 실행되는 반면 다른 코어는 상대적으로 유휴 상태를 유지하고 재생 가능한 프레임 속도보다 낮은 성능을 발휘합니다.

기존 그래픽 API는 렌더링 상태를 설정하고 단일 스레드 및 단일 컨텍스트에서 그리기 지침을 호출하며 그리기 지침은 차단됩니다. CPU와 GPU는 병렬로 실행될 수 없으며 다른 CPU 코어도 유휴 대기 상태에 있습니다.
이러한 기존 그래픽 API 아키텍처 멀티 스레드 렌더링에서는 CPU와 GPU가 서로 기다리지 않도록 분리하기 위해 소프트웨어 수준에서 시작하여 여러 스레드를 열어 로직과 렌더링 명령을 별도로 처리해야 합니다. 이미 SIGGraph2008에 DirectX9 및 10에서 소프트웨어 수준의 멀티스레드 렌더링을 구현하는 방법을 구체적으로 설명하는 Talk(Practical Parallel Rendering with DirectX 9 및 10)가 있었습니다. 핵심 부분은 D3D 드로잉 명령(Command)을 소프트웨어 계층에 기록(재생)하는 것입니다.

DirectX 9 및 10을 사용한 Practical Parallel Rendering에서 제안된 소프트웨어 수준의 멀티스레드 렌더링 아키텍처입니다.
그러나 이러한 소프트웨어 수준의 명령 기록에는 여러 가지 문제점이 있다. 일부 그래픽 API(예: 상태 쿼리)를 지원하지 않으며 그리기 지침을 기록하기 위해 추가 명령 버퍼가 필요하며 명령 단계에서 실제 GPU 리소스를 생성할 수 없습니다.
DirectX11은 하드웨어 수준에서 멀티스레드 렌더링 문제를 해결하려고 시도합니다. 즉시 컨텍스트(Immediate Context) 및 **지연 컨텍스트(Deferred Context)**라는 두 가지 장치 컨텍스트를 지원합니다. 서로 다른 지연된 컨텍스트를 서로 다른 스레드에서 동시에 사용하여 "즉시 컨텍스트"에서 실행될 명령 목록을 생성할 수 있습니다. 이 멀티스레딩 전략을 사용하면 복잡한 시나리오를 동시 작업으로 나눌 수 있습니다.

DirectX11 멀티스레딩 모델.
서로 다른 지연된 컨텍스트를 서로 다른 스레드에서 동시에 사용하여 즉각적인 컨텍스트에서 실행될 명령 목록을 생성할 수 있습니다. 이 멀티스레딩 전략을 사용하면 복잡한 시나리오를 동시 작업으로 나눌 수 있습니다. 또한 지연된 컨텍스트는 일부 드라이버의 지원을 통해 즉각적인 컨텍스트에서 명령 목록을 실행하지 않고도 하드웨어 수준 가속을 달성할 수 있습니다.
직접 컨텍스트를 사용하여 그리기 지침을 호출하는 것보다 지연된 컨텍스트의 명령 목록을 사용하여 그리기 지침을 미리 기록하는 것이 더 효율적인 이유는 무엇입니까?
그 대답은 Command List가 내부적으로 기록된 명령을 고도로 최적화한다는 사실에 있습니다. 이러한 최적화된 명령어를 실행하면 효율성이 크게 향상되며, 이는 각 개별 명령어에 대해 그래픽 API를 직접 호출하는 것보다 훨씬 더 효율적입니다.
D3D11에서는 명령 목록의 명령이 즉시 실행되지 않고 빠르게 기록됩니다. 프로그램이 ExecuteCommandList 메서드를 호출할 때까지(호출은 기다리지 않고 반환됨) 실제로 GPU에 의해 실행되지 않습니다. 이때 디퍼드 렌더링 장치 인터페이스를 사용하는 CPU 스레드와 메인 렌더링 스레드는 다음 프레임의 충돌 감지, 물리적 변환, 애니메이션 보간, 조명 준비 등을 계속하여 기록을 준비하고 새로운 명령 목록을 형성하는 등 다른 작업을 수행할 수 있습니다.
그러나 DirectX11의 멀티 스레드 아키텍처 기반에서는 하드웨어 수준의 가속이 반드시 지원되는 것은 아니기 때문에 지연 컨텍스트에 기록된 모든 명령과 직접 컨텍스트의 명령을 GPU에 제출해야 동일한 스레드(일반적으로 렌더링 스레드)에서 실행됩니다.

DirectX11에서의 다중 스레드 아키텍처의 개략도.
이러한 종류의 비하드웨어 지원 멀티스레드 렌더링은 CPU 시간(멀티스레드 기록 지침 및 도면 동기화 대기)의 일부만 절약할 뿐이며 하드웨어 수준에서 멀티스레딩의 성능을 제대로 발휘할 수 없습니다.
2.2.2 DirectX12의 멀티스레딩 기능
DirectX11의 과도기적 의사 멀티스레딩 모델(당시 대부분의 드라이버가 DirectX11의 하드웨어 수준 멀티스레딩을 지원하지 않았기 때문에 의사라고 함)과 비교하여 DirectX 12 멀티스레딩은 API 호출의 추가 오버헤드를 크게 줄여 크게 개선되었습니다. DirectX 11의 장치 컨텍스트 개념을 취소하고 명령 목록을 직접 사용하여 D3D API를 호출한 다음 명령 대기열을 통해 명령 목록을 GPU에 제출하며 모든 DirectX 12 그래픽 카드는 DirectX 12 다중 스레드 하드웨어 가속을 지원합니다.

DirectX12 멀티스레딩 모델.
원칙적으로 DirectX12 및 DirectX11 다중 스레드 렌더링 프레임워크는 유사합니다. 둘 다 서로 다른 CPU 스레드에 명령 목록(명령 목록)을 기록하고 최종적으로 이를 균일하게 실행하여 멀티 스레드 렌더링을 완료합니다. 그들은 모두 터무니없는 Draw Call 동기 호출을 근본적으로 보호하고 대신 CPU와 GPU에 의한 완전한 비동기(병렬) 실행 방법을 사용하여 전반적인 렌더링 효율성과 성능을 크게 향상시킵니다.
DirectX12의 경우 사용자 수준에는 복사 대기열, 계산 대기열 및 3D 대기열의 세 가지 유형의 명령 대기열(명령 대기열)이 있습니다. 병렬로 실행될 수 있으며 Fence, Signal 또는 Barrier를 통해 대기 및 동기화할 수 있습니다.
GPU 하드웨어 수준에는 복사 엔진, 컴퓨팅 엔진, 3D 엔진이라는 세 가지 유형의 엔진이 있습니다. 또한 병렬로 실행하고 울타리, 신호 또는 장벽을 통해 대기하고 동기화할 수도 있습니다.
명령 대기열은 GPU 하드웨어의 여러 엔진을 구동할 수 있지만 특정 제한 사항이 있습니다. 보다 구체적으로 말하면, 3D 대기열은 GPU 하드웨어의 세 가지 엔진을 구동할 수 있고, 컴퓨팅 대기열은 컴퓨팅 엔진과 복사 엔진만 구동할 수 있으며, 복사 대기열은 복사 엔진만 구동할 수 있습니다.
CPU 수준에서는 여러 개의 스레드가 있을 수 있으며, 각 스레드는 여러 개의 명령 목록(Command List)을 생성하고 생성할 수 있습니다. 각 명령 목록은 세 가지 유형의 명령 대기열 중 하나를 입력할 수 있습니다. 이러한 명령이 GPU에 의해 실행되면 각 명령 목록의 명령이 서로 다른 GPU 엔진으로 푸시되어 병렬로 실행될 수 있습니다. (아래 사진)

DirectX12의 CPU 스레드, 명령 목록, 명령 대기열 및 GPU 엔진 간의 작동 메커니즘에 대한 개략도.
2.2.3 Vulkan의 멀티스레딩 기능
크로스 플랫폼 그래픽 API의 새로운 대표자로서 Vulkan은 기존 그래픽 API의 단점을 버리고 멀티 코어 시대의 장점에 직면하여 디자인 및 아키텍처 측면에서 병렬 렌더링의 힘을 활용합니다.
종합해보면 Vulkan과 DirectX12는 매우 가깝습니다. 둘 다 Command Buffer, CommandPool, Command Queue 및 Fence와 같은 핵심 개념을 공유합니다. 병렬 모드도 매우 유사합니다. 명령 버퍼 명령은 서로 다른 CPU 스레드에서 병렬로 생성되고, 마지막으로 메인 스레드는 이러한 명령 버퍼를 수집하여 GPU에 제출합니다.

Vulkan 그래픽 API 병렬 다이어그램.
또한 Vulkan의 CommandPool은 각 프레임마다 다른 스레드로 생성되어 동기화 대기를 줄이고 병렬 효율성을 향상시킬 수 있습니다.

다른 프레임 간 Vulkan의 CommandPool 병렬 다이어그램.
또한 Vulkan에는 DirectX12와 유사한 다양한 동기화 메커니즘도 있습니다.

Vulkan 동기화 메커니즘: 세마포어(신호)는 큐를 동기화하는 데 사용됩니다. 펜스(fence)는 GPU와 CPU를 동기화하는 데 사용됩니다. Event(이벤트)와 Barrier(배리어)는 Command Buffer를 동기화하는데 사용됩니다.
Vulkan의 자세한 사용법, 분석 및 비교는 Vulkan의 멀티스레딩 평가 문서를 참조하세요.
2.2.4 Metal의 멀티스레딩 기능
iOS 및 MacOS 시스템용 독점 그래픽 API인 Metal은 차세대를 대표하는 제품이기도 합니다. OpenGL과 같은 기존 그래픽 API 사용과 호환되며 Vulkan 및 DirectX12와 같은 차세대 그래픽 API 개념 및 아키텍처도 지원합니다. 사용자 관점에서 보면 Metal은 비교적 친숙하며 더 명확한 구조와 더 친숙한 개념을 갖춘 API를 제공합니다.

OpenGL에서 차세대 그래픽 API로 마이그레이션할 때의 비용과 이점을 비교합니다. 가로축은 OpenGL(또는 ES)에서 다른 그래픽 API를 마이그레이션하는 데 드는 비용이고 세로축은 잠재적인 성능 향상입니다. Metal의 마이그레이션 비용이 더 낮다는 것을 알 수 있지만 잠재적 성능 비율은 Vulkan 및 DirectX12만큼 높지 않습니다.
Vulkan 및 DirectX와 마찬가지로 Metal에는 명령, 명령 버퍼, 명령 대기열 및 다양한 동기화 메커니즘과 같은 유사한 개념이 많이 있습니다.

메탈의 기본 개념 간의 관계 목록입니다. 명령 인코더에는 MTLRenderCommandEncoder, MTLComputeCommandEncoder 및 MTLBlitCommandEncoder의 세 가지 유형이 있습니다. CommandEncoder는 명령을 기록한 후 명령 버퍼에 저장되고 마지막으로 명령 대기열 명령 대기열에 들어갑니다.
유사한 개념과 메커니즘을 통해 Metal은 멀티 스레드 녹음 명령을 쉽게 구현할 수 있으며 하드웨어 수준에서 멀티 스레드 예약을 지원합니다.

메탈 멀티스레딩 모델 다이어그램. 그림은 서로 다른 유형의 인코더를 동시에 기록하는 세 개의 CPU 스레드를 보여줍니다. 각 스레드에는 자체 명령 버퍼가 있습니다. 마지막으로 이러한 명령 버퍼는 명령 대기열로 균일하게 가져오고 실행을 위해 GPU로 전달됩니다.
2.3 게임 엔진의 멀티스레드 렌더링
UE의 멀티스레드 렌더링을 공식적으로 설명하기 전에 먼저 다른 주류 상용 엔진의 멀티스레드 아키텍처와 디자인을 이해해 보겠습니다.
2.3.1 유니티
Unity의 렌더링 시스템에는 몇 가지 핵심 개념이 있습니다. 하나는 메인 스레드(논리 스레드)에서 실행되고 렌더링 지침 생성을 담당하는 클라이언트입니다. 다른 하나는 메인 스레드와 같은 다양한 하위 작업을 처리하거나 렌더링 명령을 생성하는 데 사용되는 작업자 스레드입니다. Unity의 렌더링 아키텍처는 다음 모드를 지원합니다.
- 단일 스레드 렌더링
단일 스레드 렌더링 모드. 이 모드에는 클라이언트 구성 요소가 하나만 있고 작업자 스레드는 없습니다. 유일한 클라이언트는 메인 스레드에서 모든 렌더링 명령(RCMD)을 생성하고 그래픽 장치 개체를 소유합니다. 또한 기본 스레드의 그래픽 장치에 대한 그래픽 API 명령(그래픽 API, GCMD)을 생성하고 호출합니다. 스케줄링 다이어그램은 다음과 같습니다.

이 모드에서는 CPU와 GPU가 서로를 기다릴 수 있어 멀티 코어 CPU를 완전히 활용하지 못하며 성능 비교가 최악입니다.
- 멀티스레드 렌더링
멀티스레드 렌더링 모드. 단일 스레드 모드와 비교하여 이 모드에는 작업 스레드가 하나 더 있습니다. 즉, GCMD를 생성하는 데 사용되는 렌더링 스레드입니다. 렌더링 스레드는 해당 플랫폼에 대한 그래픽 API 명령 생성을 전담하는 GfxDeviceClient 개체를 실행합니다.

- 작업화된 렌더링
작업 기반 렌더링 모드. 이 모드에는 여러 개의 클라이언트 개체와 단일 렌더링 스레드가 있습니다. 또한 즉각적인 그래픽 명령(IGCMD)을 생성하는 데 사용되는 전용 독립 스레드에서 각각 실행되는 여러 작업 개체가 있습니다. 또한 작업자 스레드에서 생성된 IGCMD를 그래픽 API의 GCMD로 변환하는 데 사용되는 작업자 스레드(렌더링 스레드)가 있습니다. 작동 다이어그램은 다음과 같습니다.

- 그래픽 작업
그래픽 작업 렌더링 모드. 이 모드에는 여러 클라이언트, 여러 작업자 스레드가 있고 렌더링 스레드가 없습니다. 메인 스레드의 여러 클라이언트 개체는 작업 스레드의 해당 그래픽 장치 개체를 구동하고 GCMD를 직접 생성하므로 Jobified Rendering 모드에서 IGCMD 중간 명령이 생성되지 않습니다. DirectX12, Vulkan 등과 같이 하드웨어 수준의 멀티스레딩을 지원하는 그래픽 API에서만 활성화할 수 있습니다. 작동 다이어그램은 다음과 같습니다.

2.3.2 동상
Frostbite 엔진 초기에는 각 프레임이 자르기, 구성, 렌더링의 단계로 나누어졌습니다. 각 단계에 필요한 데이터는 더블 버퍼에 담아 캐스케이드 모드로 실행해 간단한 동기화 프로세스를 적용했다. 작동 다이어그램은 다음과 같습니다.

수년간의 발전 끝에 Frostbite는 지난 몇 년간 프레임 그래프의 멀티스레드 렌더링 모드를 채택했습니다. 이 모드는 추가 분리 및 최적화를 위해 상위 계층 렌더링 논리(렌더러) 및 하위 계층 리소스(Shader, RenderContext, 그래픽 API 등)에서 엔진의 다양한 렌더링 기능(기능)을 분리하는 것을 목표로 합니다. 가장 중요한 최적화는 멀티스레드 렌더링을 활성화하는 것입니다.

FrameGraph는 프레임에 사용되는 모든 정보를 포함하는 렌더 패스 및 리소스를 상위 수준으로 표현한 것입니다. 패스 간의 순서 및 종속 관계를 지정할 수 있습니다. 다음 그림은 예입니다.

Frost 엔진은 프레임 그래프를 사용하여 디퍼드 렌더링(Deferred Shading) 시퀀스 및 종속성 그래프를 구현합니다.
프레임 다이어그램의 각 프레임 정보는 설정(Setup), 컴파일(Compile), 실행(Execute)의 세 단계로 구성됩니다.
설정 단계에서는 각 렌더 패스, 입력 텍스처, 출력 텍스처, 종속 리소스 및 기타 정보를 생성합니다.
컴파일 단계의 작업은 주로 사용되지 않는 렌더 패스와 리소스를 제거하고, 리소스 수명 주기를 계산하고, 사용 태그를 기반으로 해당 GPU 리소스를 생성하는 것입니다. 비디오 메모리 할당 알고리즘 단순화, 처음 사용 시 마지막 사용 후 릴리스 적용, 외부 리소스의 수명 주기 비동기식 계산, 바인딩 태그를 기반으로 한 정확한 리소스 관리, GPU 캐시 적중률 향상을 위한 모든 리소스에 대한 참조 평면화 등 GPU 리소스 생성 시 많은 최적화가 이루어졌습니다. 컴파일 단계에서는 모든 RenderPass의 선형 순회를 사용합니다. 순회 중에 리소스 참조 수, 리소스의 첫 번째 및 마지막 사용자, 비동기 대기 지점, 리소스 장벽 등이 계산됩니다.
실행 단계는 Setup 순서대로 실행되며(컴파일 단계는 재정렬되지 않음), 제거되지 않은 렌더 패스만 순회하여 해당 콜백 함수가 실행됩니다. 즉시 모드인 경우 장치 컨텍스트의 API를 직접 호출합니다. 실행 단계에서만 컴파일 단계에서 생성된 핸들을 기반으로 GPU 리소스를 실제로 얻을 수 있습니다.
가장 중요한 것은 전체 프로세스가 종속성 그래프(Dependency Grahp)를 통해 자동화된 비동기 계산을 구현한다는 것입니다. 비동기식 메커니즘은 기본 타임라인에서 시작하여 서로 다른 대기열의 리소스를 자동으로 동기화하고 수명 주기를 연장하여 우발적인 릴리스를 방지합니다. 물론 이 자동화된 시스템에는 일정량의 추가 메모리를 추가하는 등 예측할 수 없는 성능 병목 현상이 발생할 수 있는 부작용도 있습니다. 따라서 Frostbite 엔진은 비동기 작업이 예상대로 수행되는 방식을 제어하고 변경하는 수동 모드를 지원하여 렌더 패스별 렌더 패스 기반으로 선택적으로 참여할 수 있습니다.
다음 그림은 비동기 컴퓨팅의 작동 메커니즘을 보다 간결하고 명확하게 보여줍니다.

프로스트 엔진 비동기식 계산 다이어그램. SSAO 및 SSAO 필터의 패스는 비동기 대기열에 배치되며 Raw AO의 텍스처를 쓰고 읽습니다. 동기화 지점 이전에 종료되더라도 Raw AO의 수명 주기는 동기화 지점까지 연장됩니다.
간단히 말해서, 프레임 그래프의 렌더링 아키텍처는 프레임의 모든 정보를 기록함으로써 이점을 얻을 수 있으므로 리소스 앨리어싱(Resource Aliasing)을 통해 많은 메모리와 비디오 메모리를 절약할 수 있고 반자동 비동기 계산을 실현할 수 있으며 렌더링 파이프라인 제어를 단순화할 수 있으며 더 나은 시각화 및 진단 도구를 생성할 수 있습니다.
2.3.3 Naughty Dog 엔진
Naughty Dog의 게임 엔진은 또한 운영 체제를 사용하므로 GPU가 아닌 측 로직 코드를 운영 체제에 추가할 수 있습니다. 작업은 호출자에게 메모리 관리 세부 정보를 숨기고 간단하고 사용하기 쉬운 API를 제공하며 성능 최적화를 최우선으로 하여 다른 작업을 직접 시작하고 기다릴 수 있습니다.
운영 체제는 파이버에서 실행되며 각 파이버는 로컬 스레드와 유사합니다. 사용자 계층은 스택 공간을 제공하며 해당 컨텍스트에는 레지스터 점유를 줄이기 위해 소량의 파이버 상태가 포함됩니다. 실제로 스레드, 협력적 멀티스레딩 모델에서 실행됩니다. 파이버는 시스템 수준이 아닌 스레드이므로 컨텍스트 전환이 매우 빠르고 레지스터 상태(프로그램 수, 스택 포인터, gpr 등)만 저장 및 복원하므로 오버헤드가 매우 작습니다.
운영 체제는 여러 작업 스레드를 열고 각 작업 스레드는 GPU 하드웨어 코어에 고정됩니다. 스레드는 실행 단위이고 파이버는 컨텍스트입니다. 작업은 항상 스레드 컨텍스트 내에서 실행되며 원자 카운터를 사용하여 동기화됩니다. 다음 그림은 Naughty Dog 엔진의 운영 체제 아키텍처 다이어그램입니다.

Naughty Dog 엔진 운영 체제 아키텍처 다이어그램. 6개의 작업자 스레드, 160개의 파이버 및 3개의 작업 대기열이 있습니다.
작업은 작업 큐에 새 작업을 추가할 수 있으며 대기 중인 작업은 특별한 대기 목록에 배치됩니다. 각 대기 작업에는 참조 횟수가 있습니다. 실행을 계속하기 위해 참조 카운트가 0에 도달할 때까지 대기 큐에서 큐에서 제거되지 않습니다.

Naughty Dog 엔진 내에서는 게임 객체 업데이트, 모션 업데이트 및 믹싱, 광선 감지, 렌더링 명령 생성 등 IO 스레드를 제외한 모든 작업이 작업입니다. 운영 체제가 완전히 활용되고 병렬 비율과 효율성이 최대화되는 것을 볼 수 있습니다.
프레임 레이트를 높이기 위해 게임 로직과 렌더링 로직을 분리해 병렬로 실행하지만 서로 다른 프레임의 데이터를 처리한다. 일반적으로 게임 데이터는 렌더링 데이터보다 한 프레임 앞서고, 렌더링 로직은 GPU 데이터보다 한 프레임 앞서게 됩니다.

이러한 메커니즘을 통해 CPU 스레드 간, CPU와 GPU 간 동기화 및 대기를 방지하여 프레임 속도와 처리량을 향상시킬 수 있습니다.
또한 태그된 메모리 힙(Tagged Heap)을 도입하는 등 메모리 할당도 신중하게 관리됩니다. 메모리 힙은 2M 블록(Blocks)으로 나누어지고, 각 블록에는 레이블(Game, Render, GPU 중 하나)이 있습니다. 할당자가 메모리를 할당하고 해제할 때 운영 체제에서 직접 메모리를 가져오는 것을 방지하기 위해 레이블 힙에서 실행됩니다.

또한 할당자는 각 작업자 스레드에 대한 전용 블록 할당(TLS와 유사)을 지원하여 데이터 동기화 및 대기 시간을 방지하고 데이터 경합을 방지합니다.

2.3.4 데스티니의 엔진
데스티니(Destiny)는 1인칭 액션 롤플레잉 MMORPG입니다. 사용하는 엔진은 데스티니 엔진(Destiny's Engine)이라고도 합니다.
데스티니 엔진은 멀티 스레드 아키텍처를 기반으로 작업 기반 병렬 처리, 작업 관리 설계, 동기 처리 등의 기술을 사용하며 작업 실행도 파이버에서 이루어집니다. 작업을 실행하는 운영 체제의 우선순위는 FIFO(선입선출)입니다. 작업 그래프는 이기종 아키텍처입니다. 작업 간에는 종속성이 있지만 장벽은 없습니다.

각 프레임을 게임 개체 시뮬레이션, 개체 자르기, 렌더링 명령 생성, GPU 관련 작업 수행 및 표시 등 여러 단계로 나눕니다. 스레드 설계 측면에서 6개의 시스템 스레드가 생성되며 각 스레드의 내용은 시뮬레이션 루프, 기타 작업, 렌더링 루프, 오디오 루프, 작업 코어 및 디버깅 로그, 비동기 작업, IO 등입니다.

프레임 간 데이터 처리 시 게임 시뮬레이션과 렌더링 로직도 분리됩니다. 게임 시뮬레이션은 항상 다른 프레임보다 한 프레임 먼저 렌더링됩니다. 게임이 시뮬레이션된 후 다음 프레임 렌더링을 위해 모든 데이터와 상태가 복사(미러)됩니다.

CPU와 GPU의 병렬 효율성을 극대화하기 위해 데스티니 엔진은 동적 로드 밸런싱과 스마트 작업 일괄 처리를 채택합니다. 구체적인 방법은 모든 렌더링 및 가시성 제거 작업을 작업 시스템에 추가하여 낮은 대기 시간을 유지하는 것입니다. 다음 그림은 병렬화된 컴퓨팅 뷰 작업을 보여줍니다.

또한 시뮬레이션 로직은 렌더링 로직과 분리 및 분리되며 완전한 데이터 기반 렌더링 파이프라인이 채택됩니다. 모든 정렬, 메모리 할당, 탐색 및 기타 알고리즘은 캐시 일관성(구조 소형화 및 데이터 정렬, 단일 구조 데이터를 한 번에 캐시 라인에 로드할 수 있음)을 따릅니다.
2.4 UE의 멀티스레딩 메커니즘
이 장에서는 나중에 멀티스레드 렌더링에 더 잘 들어갈 수 있도록 UE의 멀티스레드 기반, 디자인 및 아키텍처를 주로 분석합니다.
2.4.1 UE의 멀티스레딩 기본
- TAtomic
UE의 원자적 작업은 C++의 Atomic 템플릿을 사용하지 않고 TAtomic이라는 자체 세트를 구현합니다. 제공되는 기능에는 로딩, 저장, 할당 및 기타 작업이 포함됩니다. 기본 구현에서는 플랫폼 관련 원자적 작업 인터페이스를 사용하여 구현됩니다.
// Engine\Source\Runtime\Core\Public\Templates\Atomic.h
template <typename T>
FORCEINLINE T Load(const volatile T* Element)
{
// 플랫폼의 인터페이스 로드 원자적 .
auto Result = FPlatformAtomics::AtomicRead((volatile TUnderlyingIntegerType_T<T>*)Element);
return *(const T*)&Result;
}
template <typename T>
FORCEINLINE void Store(const volatile T* Element, T Value)
{
// 플랫폼의 인터페이스 저장(Store)원자적 .
FPlatformAtomics::InterlockedExchange((volatile TUnderlyingIntegerType_T<T>*)Element, *(const TUnderlyingIntegerType_T<T>*)&Value);
}
template <typename T>
FORCEINLINE T Exchange(volatile T* Element, T Value)
{
// 플랫폼의 인터페이스 원자적 .
auto Result = FPlatformAtomics::InterlockedExchange((volatile TUnderlyingIntegerType_T<T>*)Element, *(const TUnderlyingIntegerType_T<T>*)&Value);
return *(const T*)&Result;
}메모리 순서 측면에서 4가지 모드를 제공하는 C++와 달리 UE는 이를 단순화하여 2가지 모드만 제공합니다.
enum class EMemoryOrder
{
Relaxed, // , 정렬
SequentiallyConsistent //
};TAtomic은 템플릿 클래스이지만 기본 유형에만 적용된다는 점에 유의해야 합니다. UE는 탐지 목적을 달성하기 위해 상위 클래스 TAtomicBaseType_T를 사용합니다.
template <typename T>
class TAtomic final : public UE4Atomic_Private::TAtomicBaseType_T<T>
{
static_assert(TIsTrivial<T>::Value, "TAtomic is only usable with trivial types");
(......)
}- 미래
UE는 템플릿 클래스이고 반환 값 유형을 추상화하는 C++와 유사한 Future 및 Promise 개체를 구현합니다. 다음은 TFuture의 설명입니다.
// Engine\Source\Runtime\Core\Public\Async\Future.h
template<typename InternalResultType>
class TFutureBase
{
public:
bool IsReady() const;
bool IsValid() const;
void Wait() const
{
if (State.IsValid())
{
while (!WaitFor(FTimespan::MaxValue()));
}
}
bool WaitFor(const FTimespan& Duration) const
{
return State.IsValid() ? State->WaitFor(Duration) : false;
}
bool WaitUntil(const FDateTime& Time) const
{
return WaitFor(Time - FDateTime::UtcNow());
}
protected:
typedef TSharedPtr<TFutureState<InternalResultType>, ESPMode::ThreadSafe> StateType;
const StateType& GetState() const;
template<typename Func>
auto Then(Func Continuation);
template<typename Func>
auto Next(Func Continuation);
void Reset();
private:
/** Holds the future's state. */
StateType State;
};
template<typename ResultType>
class TFuture : public TFutureBase<ResultType>
{
typedef TFutureBase<ResultType> BaseType;
public:
ResultType Get() const
{
return this->GetState()->GetResult();
}
TSharedFuture<ResultType> Share()
{
return TSharedFuture<ResultType>(MoveTemp(*this));
}
};- T약속
TPromise는 일반적으로 아래와 같이 TFuture와 함께 사용됩니다.
template<typename InternalResultType>
class TPromiseBase : FNoncopyable
{
typedef TSharedPtr<TFutureState<InternalResultType>, ESPMode::ThreadSafe> StateType;
(......)
protected:
const StateType& GetState();
private:
StateType State; // 저장(Store)Future의 상태 .
};
template<typename ResultType>
class TPromise : public TPromiseBase<ResultType>
{
public:
typedef TPromiseBase<ResultType> BaseType;
public:
// 페치/가져오기(Fetch)Future객체/오브젝트
TFuture<ResultType> GetFuture()
{
check(!FutureRetrieved);
FutureRetrieved = true;
return TFuture<ResultType>(this->GetState());
}
// 설정(Set)Future의
FORCEINLINE void SetValue(const ResultType& Result)
{
EmplaceValue(Result);
}
FORCEINLINE void SetValue(ResultType&& Result)
{
EmplaceValue(MoveTemp(Result));
}
template<typename... ArgTypes>
void EmplaceValue(ArgTypes&&... Args)
{
this->GetState()->EmplaceResult(Forward<ArgTypes>(Args)...);
}
private:
bool FutureRetrieved;
};- 병렬용
ParallelFor는 멀티스레드 병렬 처리 작업을 지원하는 UE 내장 For 루프입니다. 렌더링 시스템에서 매우 일반적으로 사용됩니다. 다음과 같은 병렬 방법을 지원합니다.
enum class EParallelForFlags
{
None, // 기본값(Default)병렬
ForceSingleThread = 1, // 스레드(Thread), 을(를) 활용하여 .
Unbalanced = 2, // , 을(를) 활용하여 계산/산출(Calculate)의 .
PumpRenderingThread = 4, // 렌더링(Render)스레드(Thread). 만약 는 렌더링(Render)스레드(Thread)호출(Call), ProcessThread상태 .
};지원되는 ParallelFor 호출 메서드는 다음과 같습니다.
inline void ParallelFor(int32 Num, TFunctionRef<void(int32)> Body, bool bForceSingleThread, bool bPumpRenderingThread=false);
inline void ParallelFor(int32 Num, TFunctionRef<void(int32)> Body, EParallelForFlags Flags = EParallelForFlags::None);
template<typename FunctionType>
inline void ParallelForTemplate(int32 Num, const FunctionType& Body, EParallelForFlags Flags = EParallelForFlags::None);
inline void ParallelForWithPreWork(int32 Num, TFunctionRef<void(int32)> Body, TFunctionRef<void()> CurrentThreadWorkToDoBeforeHelping, bool bForceSingleThread, bool bPumpRenderingThread = false);
inline void ParallelForWithPreWork(int32 Num, TFunctionRef<void(int32)> Body, TFunctionRef<void()> CurrentThreadWorkToDoBeforeHelping, EParallelForFlags Flags = EParallelForFlags::None);ParallelFor는 TaskGraph 메커니즘을 기반으로 구현됩니다. TaskGraph는 나중에 언급되므로 여기서는 구현을 다루지 않습니다. UE의 적용 사례는 다음과 같습니다.
// Engine\Source\Runtime\Engine\Private\Components\ActorComponent.cpp
// 병렬 추가(Add)Primitive까지 의 을(를) 활용하여 .
void FRegisterComponentContext::Process()
{
FSceneInterface* Scene = World->Scene;
ParallelFor(AddPrimitiveBatches.Num(), // 개수
[&](int32 Index) //함수 , Index반환(Return)
{
if (!AddPrimitiveBatches[Index]->IsPendingKill())
{
Scene->AddPrimitive(AddPrimitiveBatches[Index]);
}
},
!FApp::ShouldUseThreadingForPerformance() // 는 스레드(Thread)처리(Process)
);
AddPrimitiveBatches.Empty();
}- 기본 템플릿
UnrealTemplate.h는 데이터 변환, 복사, 전송 및 기타 기능을 위한 많은 기본 템플릿을 정의합니다. 다음은 일반적인 함수 및 유형의 몇 가지 예입니다.
템플릿 이름 분석하다 stl 매핑
템플릿 ReferencedTypeIfAThenAElseB(ReferencedType A,ReferencedType* B) A를 반환 ? 답 : B
- 템플릿
void Move(T& A, 유형 이름 TMoveSupportTraits::복사 B)
A를 해제하고 B의 데이터를 A로 바꾸지만 B의 데이터는 영향을 받지 않습니다.
-
템플릿
void Move(T& A, 유형 이름 TMoveSupportTraits::Move B)
A를 해제하고 B의 데이터를 A로 대체하지만 B의 데이터에 영향을 미칩니다.
-
F복사 불가능
복사할 수 없는 객체를 구현하기 위해 파생시킵니다.
-
TGuard값
작동 범위가 있는 값의 경우 새 값과 이전 값을 지정할 수 있습니다. 새 값은 범위 내에 있으며 범위를 벗어나면 이전 값이 됩니다.
-
TScope카운터
범위가 있는 카운터, 범위 내에서 카운터는 +1이고 범위를 떠난 후에는 -1입니다.
-
템플릿
유형 이름 TRemoveReference::Type&& MoveTemp(T&& Obj)
참조를 rvalue로 변환하여 소스 값을 수정할 수 있습니다.
표준::이동
템플릿
T CopyTemp(T& Val)
소스 값을 변경하지 않고 rvalue 복사본을 강제로 생성합니다.
-
템플릿
T&& 전달(유형 이름 TRemoveReference::Type& Obj)
참조를 rvalue 참조로 변환합니다.
표준::앞으로
템플릿 <유형 이름 T, 유형 이름 ArgType>
T StaticCast(ArgType&& Arg)
정적 유형 변환.
static_cast2.4.2 UE의 멀티스레드 구현
UE의 멀티 스레드 구현은 C++11 표준 라이브러리를 채택하지 않지만 시스템 스레드, 스레드 풀, 비동기 작업, 작업 그래프, 관련 알림 및 동기화 메커니즘을 포함하여 시스템 수준에서 이를 캡슐화하고 구현합니다.
2.4.2.1 FRunnable
FRunnable은 여러 스레드에서 병렬로 실행될 수 있는 모든 개체의 상위 클래스입니다. 제공되는 기본 인터페이스는 다음과 같습니다.
// Engine\Source\Runtime\Core\Public\HAL\Runnable.h
class CORE_API FRunnable
{
public:
virtual bool Init(); // 초기화(Initialize), 반환(Return)True.
virtual uint32 Run(); // , 오직 Init호출(Call).
virtual void Stop(); // .
virtual void Exit(); // 탈출/종료(Exit), 데이터 .
};FRunnable과 그 하위 클래스는 단일 스레드에서만 실행되는 FSingleThreadRunnable 클래스와 달리 여러 스레드에서 실행될 수 있는 개체입니다.
// Engine\Source\Runtime\Core\Public\Misc\SingleThreadRunnable.h
// 스레드(Thread)비활성화(Disable)의 스레드(Thread)의
class CORE_API FSingleThreadRunnable
{
public:
virtual void Tick();
};FRunnable에는 많은 하위 클래스가 있습니다. 다음은 몇 가지 일반적인 핵심 하위 클래스와 해당 분석입니다.
FRenderingThread: 렌더링 스레드에서 실행 중인 개체입니다. 나중에 구체적으로 분석할 장들이 있을 것입니다.
FRHIThread: RHI 스레드에서 실행되는 개체입니다. 나중에 구체적으로 분석할 장들이 있을 것입니다.
FRenderingThreadTickHeartbeat: 하트비트 렌더링 스레드에서 실행되는 개체입니다.
FTaskThreadBase: 스레드에서 실행되는 작업의 상위 클래스입니다. 나중에 이 부분을 분석하는 장이 있을 것이다.
FQueuedThread: 스레드 풀에 저장할 수 있는 스레드 상위 클래스입니다. 제공되는 인터페이스는 다음과 같습니다.
// Engine\Source\Runtime\Core\Private\HAL\ThreadingBase.cpp
class FQueuedThread : public FRunnable
{
protected:
FEvent* DoWorkEvent; // 실행(Execute)의 이벤트 .
TAtomic<bool> TimeToDie; // 는 .
IQueuedWork* volatile QueuedWork; // 실행(Execute)의 .
class FQueuedThreadPoolBase* OwningThreadPool; // 의 스레드(Thread).
FRunnableThread* Thread; // 을(를) 활용하여 실행(Execute)의 스레드(Thread).
virtual uint32 Run() override;
public:
virtual bool Create(class FQueuedThreadPoolBase* InPool,uint32 InStackSize,EThreadPriority ThreadPriority);
bool KillThread();
void DoWork(IQueuedWork* InQueuedWork);
};- TAsyncRunnable: 별도의 스레드에서 비동기적으로 실행되는 작업입니다. 템플릿 클래스이며 다음과 같이 선언됩니다.
// Engine\Source\Runtime\Core\Public\Async\Async.h
template<typename ResultType>
class TAsyncRunnable: public FRunnable
{
public:
virtual uint32 Run() override;
private:
TUniqueFunction<ResultType()> Function;
TPromise<ResultType> Promise;
TFuture<FRunnableThread*> ThreadFuture;
};- FAsyncPurge: 워커 스레드에 있는 UObject 객체의 소멸을 제공하는 보조 클래스입니다.
FRunnable 객체는 독립적으로 존재할 수 없으며 실제로 작업을 수행하기 위해 항상 스레드에 의존한다는 것을 알 수 있습니다.
또한 FRenderingThread 및 FQueuedThread는 이름만 보면 실제 스레드처럼 들리지만 그렇지 않다는 점도 지적할 필요가 있습니다. 특정 특정 작업을 처리하는 데 사용되는 실행 가능한 개체일 뿐입니다. 실제로 실행을 위해 여전히 FRunnableThread의 내부 멤버 개체에 의존합니다.
2.4.2.2 FRunnableThread
FRunnableThread는 실행 가능한 스레드의 상위 클래스이며 스레드 수명 주기를 관리하기 위한 인터페이스 세트를 제공합니다. 제공되는 기본 인터페이스 및 분석은 다음과 같습니다.
// Engine\Source\Runtime\Core\Public\HAL\RunnableThread.h
class CORE_API FRunnableThread
{
static uint32 RunnableTlsSlot; // FRunnableThread의 TLS슬롯(Slot).
public:
static uint32 GetTlsSlot();
// 정적 , 을(를) 활용하여 생성(Create)스레드(Thread), 개 FRunnable객체/오브젝트 , 을(를) 활용하여 스레드(Thread)실행(Execute)의 .
static FRunnableThread* Create(FRunnable* InRunnable, const TCHAR* ThreadName, uint32 InStackSize = 0,
EThreadPriority InThreadPri, uint64 InThreadAffinityMask,EThreadCreateFlags InCreateFlags);
// 설정(Set)스레드(Thread)단계 .
virtual void SetThreadPriority( EThreadPriority NewPriority );
// /스레드(Thread)
virtual void Suspend( bool bShouldPause = true );
// 소멸 스레드(Thread), 플래그 bShouldWait로 true, 이면 메모리 또는 !
virtual bool Kill( bool bShouldWait = true );
// 실행(Execute), 호출(Call)스레드(Thread).
virtual void WaitForCompletion();
const uint32 GetThreadID() const;
const FString& GetThreadName() const;
protected:
FString ThreadName;
FRunnable* Runnable; // 실행(Execute)객체/오브젝트
FEvent* ThreadInitSyncEvent; // 스레드(Thread)초기화(Initialize)동기화 이벤트 , 스레드(Thread)초기화(Initialize)실행(Execute).
uint64 ThreadAffinityMask; // 및 플래그 , 을(를) 활용하여 스레드(Thread)의 CPU실행(Execute).
TArray<FTlsAutoCleanup*> TlsInstances; // 스레드(Thread)의 Tls객체/오브젝트 .
EThreadPriority ThreadPriority;
uint32 ThreadID;
private:
virtual void Tick();
};FRunnableThread는 정적 생성 인터페이스를 제공한다는 점에 유의해야 합니다. 스레드를 생성할 때 스레드가 수행할 작업으로 FRunnable 개체를 지정해야 합니다. 기본 부모 클래스입니다. 다음은 이 클래스에서 상속된 일부 핵심 하위 클래스와 해당 분석입니다.
- FRunnableThreadWin: Windows 플랫폼용 스레드 구현입니다. 인터페이스와 구현은 다음과 같습니다.
// Engine\Source\Runtime\Core\Private\Windows\WindowsRunnableThread.h
class FRunnableThreadWin : public FRunnableThread
{
HANDLE Thread; // 스레드(Thread)
// 스레드(Thread)인터페이스 , 생성(Create)스레드(Thread)로써 파라미터 .
static ::DWORD STDCALL _ThreadProc( LPVOID pThis )
{
check(pThis);
return ((FRunnableThreadWin*)pThis)->GuardedRun();
}
uint32 GuardedRun();
uint32 Run();
public:
// 변환(Transform/Convert)단계
static int TranslateThreadPriority(EThreadPriority Priority)
{
switch (Priority)
{
case TPri_AboveNormal: return THREAD_PRIORITY_HIGHEST;
case TPri_Normal: return THREAD_PRIORITY_HIGHEST - 1;
case TPri_BelowNormal: return THREAD_PRIORITY_HIGHEST - 3;
case TPri_Highest: return THREAD_PRIORITY_HIGHEST;
case TPri_TimeCritical: return THREAD_PRIORITY_HIGHEST;
case TPri_Lowest: return THREAD_PRIORITY_HIGHEST - 4;
case TPri_SlightlyBelowNormal: return THREAD_PRIORITY_HIGHEST - 2;
default: UE_LOG(LogHAL, Fatal, TEXT("Unknown Priority passed to TranslateThreadPriority()")); return TPri_Normal;
}
}
// 설정(Set)단계
virtual void SetThreadPriority( EThreadPriority NewPriority ) override
{
// Don't bother calling the OS if there is no need
ThreadPriority = NewPriority;
// Change the priority on the thread
::SetThreadPriority(Thread, TranslateThreadPriority(ThreadPriority));
}
virtual void Suspend( bool bShouldPause = true ) override
{
check(Thread);
if (bShouldPause == true)
{
SuspendThread(Thread);
}
else
{
ResumeThread(Thread);
}
}
virtual bool Kill( bool bShouldWait = false ) override
{
check(Thread && "Did you forget to call Create()?");
bool bDidExitOK = true;
// Runnable객체/오브젝트 , 데이터 의
if (Runnable)
{
Runnable->Stop();
}
// 스레드(Thread)처리(Process).
if (bShouldWait == true)
{
// Wait indefinitely for the thread to finish. IMPORTANT: It's not safe to just go and
// kill the thread with TerminateThread() as it could have a mutex lock that's shared
// with a thread that's continuing to run, which would cause that other thread to
// dead-lock. (This can manifest itself in code as simple as the synchronization
// object that is used by our logging output classes. Trust us, we've seen it!)
WaitForSingleObject(Thread,INFINITE);
}
// 비활성화(Disable)스레드(Thread)
CloseHandle(Thread);
Thread = NULL;
return bDidExitOK;
}
virtual void WaitForCompletion( ) override
{
// Block until this thread exits
WaitForSingleObject(Thread,INFINITE);
}
protected:
virtual bool CreateInternal( FRunnable* InRunnable, const TCHAR* InThreadName,
uint32 InStackSize = 0,
EThreadPriority InThreadPri = TPri_Normal, uint64 InThreadAffinityMask = 0,
EThreadCreateFlags InCreateFlags = EThreadCreateFlags::None) override
{
static bool bOnce = false;
if (!bOnce)
{
bOnce = true;
::SetThreadPriority(::GetCurrentThread(), TranslateThreadPriority(TPri_Normal)); // set the main thread to be normal, since this is no longer the windows default.
}
check(InRunnable);
Runnable = InRunnable;
ThreadAffinityMask = InThreadAffinityMask;
// 생성(Create)초기화(Initialize)동기화 이벤트 .
ThreadInitSyncEvent = FPlatformProcess::GetSynchEventFromPool(true);
ThreadName = InThreadName ? InThreadName : TEXT("Unnamed UE4");
// Create the new thread
{
LLM_SCOPE(ELLMTag::ThreadStack);
LLM_PLATFORM_SCOPE(ELLMTag::ThreadStackPlatform);
// add in the thread size, since it's allocated in a black box we can't track
LLM(FLowLevelMemTracker::Get().OnLowLevelAlloc(ELLMTracker::Default, nullptr, InStackSize));
LLM(FLowLevelMemTracker::Get().OnLowLevelAlloc(ELLMTracker::Platform, nullptr, InStackSize));
// 호출(Call)Windows API생성(Create)스레드(Thread).
Thread = CreateThread(NULL, InStackSize, _ThreadProc, this, STACK_SIZE_PARAM_IS_A_RESERVATION | CREATE_SUSPENDED, (::DWORD *)&ThreadID);
}
// If it fails, clear all the vars
if (Thread == NULL)
{
Runnable = nullptr;
}
else
{
// 추가(Add)까지 스레드(Thread) 내에서 .
FThreadManager::Get().AddThread(ThreadID, this);
ResumeThread(Thread);
// Let the thread start up
ThreadInitSyncEvent->Wait(INFINITE);
SetThreadPriority(InThreadPri);
}
// 동기화 이벤트
FPlatformProcess::ReturnSynchEventToPool(ThreadInitSyncEvent);
ThreadInitSyncEvent = nullptr;
return Thread != NULL;
}
};위의 코드에서 볼 수 있듯이 Windows 플랫폼의 스레드는 Windows API를 직접 호출하여 정보를 생성하고 동기화함으로써 스레드의 플랫폼 추상화를 실현하고 플랫폼 종속성을 추상화합니다.
- FRunnableThreadPThread: Linux, Solaris, Apple 등과 같은 Unix 계열 POSIX 시스템에서 일반적으로 사용되는 POSIX Thread(줄여서 PThread)의 상위 클래스입니다. 구현 방식은 Windows 플랫폼과 유사하므로 여기서는 코드 분석을 수행하지 않습니다. 그 서브클래스는 다음과 같습니다:
FRunnableThreadApple: Apple 시스템(MacOS, iOS)의 스레드입니다.
FRunnableThreadAndroid: Android 시스템 스레드입니다.
FRunnableThreadUnix: Unix 시스템의 스레드입니다.
FRunnableThreadHoloLens: HoloLens 시스템의 스레드입니다.
FFakeThread: 멀티스레딩이 비활성화된 후 대체되는 가짜 스레드는 실제로 단일 스레드에서 실행됩니다.
FRunnable과 FRunnableThread는 서로를 보완하며 필수 불가결합니다. 하나는 실행 중인 캐리어이고 다른 하나는 실행 중인 콘텐츠입니다. 다음은 적용 예입니다.
// FRunnable
class FMyRunnable : public FRunnable
{
bool bStop;
public:
virtual bool Init(void)
{
bStop = false;
return true;
}
virtual uint32 Run(void)
{
for (int32 i = 0; i < 10 && !bStop; i++)
{
FPlatformProcess::Sleep(1.0f);
}
return 0;
}
virtual void Stop(void)
{
bStop = true;
}
virtual void Exit(void)
{
}
};
void TestRunnableAndRunnableThread()
{
// 생성(Create)Runnable객체/오브젝트
FMyRunnable* MyRunnable = new FMyRunnable;
// 생성(Create)스레드(Thread), MyRunnable
FRunnableThread* MyThread = FRunnableThread::Create(MyRunnable, TEXT("MyRunnable"));
// 현재(Current)스레드(Thread)
FPlatformProcess::Sleep(4.0f);
// 스레드(Thread)
MyRunnable->Stop();
MyThread->WaitForCompletion();
// 데이터 .
delete MyThread;
delete MyRunnable;
}주의 깊은 학생들은 스레드를 생성할 때 스레드가 FThreadManager에 추가된다는 점을 알아차렸을 것입니다. 이는 모든 스레드가 FThreadManager에 의해 관리된다는 의미입니다. 다음은 FThreadManager의 선언입니다.
// Engine\Source\Runtime\Core\Public\HAL\ThreadManager.h
class FThreadManager
{
FCriticalSection ThreadsCritical; // 수정 스레드(Thread)리스트 Threads의
static bool bIsInitialized;
TMap<uint32, class FRunnableThread*, TInlineSetAllocator<256>> Threads; // 스레드(Thread)리스트 , ※ 주의: 데이터 구조체 는 Map, Key는 스레드(Thread)ID.
public:
void AddThread(uint32 ThreadId, class FRunnableThread* Thread); // 추가(Add)스레드(Thread)
void RemoveThread(class FRunnableThread* Thread); // 스레드(Thread)
void Tick(); // 갱신(Update), 오직 FFakeThread을(를) 활용하여 .
const FString& GetThreadName(uint32 ThreadId);
void ForEachThread(TFunction<void(uint32, class FRunnableThread*)> Func); // 순회(Traverse/Iterate)스레드(Thread)
static bool IsInitialized();
static FThreadManager& Get();
};2.4.2.3 대기 중인 작업
이 섹션에서는 IQueuedWork, TAsyncQueuedWork, FQueuedThreadPool, FQueuedThreadPoolBase 등을 포함한 UE의 대기 중인 QueuedWork 시스템을 설명합니다.
- IQueuedWork 및 TAsyncQueuedWork
IQueuedWork는 FQueuedThreadPool 스레드 풀 개체에 의해 실행될 대기열 작업 개체 집합을 저장하는 추상 인터페이스 집합입니다. IQueuedWork의 인터페이스는 다음과 같습니다.
// Engine\Source\Runtime\Core\Public\Misc\IQueuedWork.h
class IQueuedWork
{
public:
virtual void DoThreadedWork() = 0; // 실행(Execute)의 .
virtual void Abandon() = 0; // 실행(Execute), 의 객체/오브젝트 데이터 .
};IQueuedWork는 추상 클래스일 뿐이고 실제로 코드를 실행하지 않으므로 기본 하위 클래스인 TAsyncQueuedWork가 코드 구현 작업을 맡습니다. 다음은 TAsyncQueuedWork의 선언 및 구현입니다.
// Engine\Source\Runtime\Core\Public\Async\Async.h
template<typename ResultType>
class TAsyncQueuedWork : public IQueuedWork
{
public:
virtual void DoThreadedWork() override
{
SetPromise(Promise, Function);
delete this;
}
virtual void Abandon() override
{
// not supported
}
private:
TUniqueFunction<ResultType()> Function; // 실행(Execute)의 함수 리스트 .
TPromise<ResultType> Promise; // 을(를) 활용하여 동기화 의 객체/오브젝트
};- FQueuedThreadPool 및 FQueuedThreadPoolBase
FRunnable 및 FRunnableThread와 유사하게 TAsyncQueuedWork는 작업을 독립적으로 수행할 수 없으며 실행을 위해 FQueuedThreadPool에 의존해야 합니다. 다음은 FQueuedThreadPool의 선언입니다:
// Engine\Source\Runtime\Core\Public\Misc\QueuedThreadPool.h
// 실행(Execute)IQueuedWork리스트 의 스레드(Thread).
class FQueuedThreadPool
{
public:
// 생성(Create)개수 、크기 및 단계 의 스레드(Thread)。
virtual bool Create( uint32 InNumQueuedThreads, uint32 StackSize = (32 * 1024), EThreadPriority ThreadPriority=TPri_Normal ) = 0;
// 소멸 스레드(Thread)의 스레드(Thread).
virtual void Destroy() = 0;
// 추가(Add). 만약 을(를) 활용하여 의 스레드(Thread), 이면 즉, 실행(Execute); 이면 실행(Execute).
virtual void AddQueuedWork( IQueuedWork* InQueuedWork ) = 0;
// .
virtual bool RetractQueuedWork( IQueuedWork* InQueuedWork ) = 0;
// 페치/가져오기(Fetch)스레드(Thread)개수 .
virtual int32 GetNumThreads() const = 0;
public:
// 생성(Create)스레드(Thread)객체/오브젝트 .
static FQueuedThreadPool* Allocate();
// 크기 .
static uint32 OverrideStackSize;
};위에서 볼 수 있듯이 FQueuedThreadPool은 인터페이스만 제공하고 구현은 없는 추상 클래스입니다. 실제로 구현은 다음과 같이 FQueuedThreadPoolBase에 있습니다.
// Engine\Source\Runtime\Core\Private\HAL\ThreadingBase.cpp
class FQueuedThreadPoolBase : public FQueuedThreadPool
{
protected:
TArray<IQueuedWork*> QueuedWork; // 실행(Execute)의 리스트
TArray<FQueuedThread*> QueuedThreads; // 스레드(Thread)의 을(를) 활용하여 스레드(Thread)
TArray<FQueuedThread*> AllThreads; // 스레드(Thread)의 스레드(Thread)
FCriticalSection* SynchQueue; // 동기화
bool TimeToDie; // 플래그
public:
FQueuedThreadPoolBase()
: SynchQueue(nullptr)
, TimeToDie(0)
{ }
virtual ~FQueuedThreadPoolBase()
{
Destroy();
}
virtual bool Create(uint32 InNumQueuedThreads,uint32 StackSize = (32 * 1024),EThreadPriority ThreadPriority=TPri_Normal) override
{
// 처리(Process)동기화 .
bool bWasSuccessful = true;
check(SynchQueue == nullptr);
SynchQueue = new FCriticalSection();
FScopeLock Lock(SynchQueue);
// Presize the array so there is no extra memory allocated
check(QueuedThreads.Num() == 0);
QueuedThreads.Empty(InNumQueuedThreads);
if( OverrideStackSize > StackSize )
{
StackSize = OverrideStackSize;
}
// 생성(Create)스레드(Thread), ※ 주의: 생성(Create)의 는 FQueuedThread.
for (uint32 Count = 0; Count < InNumQueuedThreads && bWasSuccessful == true; Count++)
{
FQueuedThread* pThread = new FQueuedThread();
// 을(를) 활용하여 FQueuedThread객체/오브젝트 생성(Create)의 스레드(Thread).
if (pThread->Create(this,StackSize,ThreadPriority) == true)
{
QueuedThreads.Add(pThread);
AllThreads.Add(pThread);
}
else
{
// 생성(Create), 스레드(Thread)객체/오브젝트 .
bWasSuccessful = false;
delete pThread;
}
}
// 생성(Create)스레드(Thread), 데이터 .
if (bWasSuccessful == false)
{
Destroy();
}
return bWasSuccessful;
}
virtual void Destroy() override
{
if (SynchQueue)
{
{
FScopeLock Lock(SynchQueue);
TimeToDie = 1;
FPlatformMisc::MemoryBarrier();
// Clean up all queued objects
for (int32 Index = 0; Index < QueuedWork.Num(); Index++)
{
QueuedWork[Index]->Abandon();
}
// Empty out the invalid pointers
QueuedWork.Empty();
}
// 스레드(Thread)실행(Execute), ※ 주의: 을(를) 활용하여 동기화 , 는 을(를) 활용하여 의 메커니즘 .
while (1)
{
{
// AllThreads 및 QueuedThreads의 데이터 . 스레드(Thread)수정 데이터 .
FScopeLock Lock(SynchQueue);
if (AllThreads.Num() == QueuedThreads.Num())
{
break;
}
}
FPlatformProcess::Sleep(0.0f); // 현재(Current)스레드(Thread), 현재(Current)스레드(Thread)을(를) 활용하여 cpu.
}
// 스레드(Thread).
{
FScopeLock Lock(SynchQueue);
// Now tell each thread to die and delete those
for (int32 Index = 0; Index < AllThreads.Num(); Index++)
{
AllThreads[Index]->KillThread();
delete AllThreads[Index];
}
QueuedThreads.Empty();
AllThreads.Empty();
}
// 동기화 .
delete SynchQueue;
SynchQueue = nullptr;
}
}
int32 GetNumQueuedJobs() const
{
return QueuedWork.Num();
}
virtual int32 GetNumThreads() const
{
return AllThreads.Num();
}
// 추가(Add).
void AddQueuedWork(IQueuedWork* InQueuedWork) override
{
check(InQueuedWork != nullptr);
if (TimeToDie)
{
InQueuedWork->Abandon();
return;
}
check(SynchQueue);
FQueuedThread* Thread = nullptr;
{
// 스레드(Thread)의 데이터 .
FScopeLock sl(SynchQueue);
const int32 AvailableThreadCount = QueuedThreads.Num();
// 을(를) 활용하여 스레드(Thread), 추가(Add), 실행(Execute).
if (AvailableThreadCount == 0)
{
QueuedWork.Add(InQueuedWork);
return;
}
// 로부터 을(를) 활용하여 스레드(Thread) 내에서 페치/가져오기(Fetch)개 스레드(Thread), 로부터 을(를) 활용하여 스레드(Thread) 내에서 .
const int32 ThreadIndex = AvailableThreadCount - 1;
Thread = QueuedThreads[ThreadIndex];
QueuedThreads.RemoveAt(ThreadIndex, 1, /* do not allow shrinking */ false);
}
// 실행(Execute)
Thread->DoWork(InQueuedWork);
}
virtual bool RetractQueuedWork(IQueuedWork* InQueuedWork) override
{
if (TimeToDie)
{
return false; // no special consideration for this, refuse the retraction and let shutdown proceed
}
check(InQueuedWork != nullptr);
check(SynchQueue);
FScopeLock sl(SynchQueue);
return !!QueuedWork.RemoveSingle(InQueuedWork);
}
// 만약 을(를) 활용하여 ,이면 페치/가져오기(Fetch)개 실행(Execute), 이면 스레드(Thread)을(를) 활용하여 스레드(Thread). 인터페이스 에 의해 FQueuedThread호출(Call).
IQueuedWork* ReturnToPoolOrGetNextJob(FQueuedThread* InQueuedThread)
{
check(InQueuedThread != nullptr);
IQueuedWork* Work = nullptr;
// Check to see if there is any work to be done
FScopeLock sl(SynchQueue);
if (TimeToDie)
{
check(!QueuedWork.Num()); // we better not have anything if we are dying
}
if (QueuedWork.Num() > 0)
{
// Grab the oldest work in the queue. This is slower than
// getting the most recent but prevents work from being
// queued and never done
Work = QueuedWork[0];
// Remove it from the list so no one else grabs it
QueuedWork.RemoveAt(0, 1, /* do not allow shrinking */ false);
}
if (!Work)
{
// There was no work to be done, so add the thread to the pool
QueuedThreads.Add(InQueuedThread);
}
return Work;
}
};위 인터페이스 ReturnToPoolOrGetNextJob은 FQueuedThreadPoolBase에 의해 호출되지 않지만 아래와 같이 작업을 실행하고 완료한 FQueuedThread 객체에 의해 적극적으로 호출됩니다.
uint32 FQueuedThread::Run()
{
while (!TimeToDie.Load(EMemoryOrder::Relaxed))
{
bool bContinueWaiting = true;
(......)
// 이벤트 .
if (bContinueWaiting)
{
DoWorkEvent->Wait();
}
IQueuedWork* LocalQueuedWork = QueuedWork;
QueuedWork = nullptr;
FPlatformMisc::MemoryBarrier();
check(LocalQueuedWork || TimeToDie.Load(EMemoryOrder::Relaxed)); // well you woke me up, where is the job or termination request?
// 로부터 스레드(Thread)페치/가져오기(Fetch)실행(Execute), 까지 스레드(Thread)의 실행(Execute).
while (LocalQueuedWork)
{
// 실행(Execute).
LocalQueuedWork->DoThreadedWork();
// 로부터 스레드(Thread)페치/가져오기(Fetch)개 .
LocalQueuedWork = OwningThreadPool->ReturnToPoolOrGetNextJob(this);
}
}
return 0;
}위에서 볼 수 있듯이 FQueuedThreadPool과 FQueuedThread의 데이터와 인터페이스는 교묘하게 협력하여 작업을 병렬로 실행합니다.
- GThreadPool
스레드 풀 메커니즘이 설명되었습니다. UE의 전역 스레드 풀 GThreadPool의 초기화 과정에 대해 이야기해 보겠습니다. 이 프로세스는 FEngineLoop::PreInitPreStartupScreen, 1.4.6.1 엔진 사전 초기화에서 언급되었습니다.
// Engine\Source\Runtime\Launch\Private\LaunchEngineLoop.cpp
int32 FEngineLoop::PreInitPreStartupScreen(const TCHAR* CmdLine)
{
(......)
{
TRACE_THREAD_GROUP_SCOPE("ThreadPool");
// 생성(Create)글로벌 스레드(Thread)
GThreadPool = FQueuedThreadPool::Allocate();
int32 NumThreadsInThreadPool = FPlatformMisc::NumberOfWorkerThreadsToSpawn();
// 만약 는 , 스레드(Thread)오직 개 스레드(Thread).
if (FPlatformProperties::IsServerOnly())
{
NumThreadsInThreadPool = 1;
}
// 생성(Create)스레드(Thread)의 스레드(Thread)개수 .
verify(GThreadPool->Create(NumThreadsInThreadPool, StackSize * 1024, TPri_SlightlyBelowNormal));
}
(......)
}우리를 위해 작업을 수행하기 위해 GThreadPool이 필요한 경우 사용 예는 다음과 같습니다.
// Engine\Source\Runtime\Engine\Private\ShadowMap.cpp
// 스레드(Thread)텍스처(Texture)
if (bMultithreadedEncode)
{
// 의 .
FThreadSafeCounter Counter(PendingTextures.Num());
// 의 텍스처(Texture)리스트
TArray<FAsyncEncode<FShadowMapPendingTexture>> AsyncEncodeTasks;
AsyncEncodeTasks.Empty(PendingTextures.Num());
// 생성(Create), 추가(Add)까지 AsyncEncodeTasks리스트 내에서 .
for (auto& PendingTexture : PendingTextures)
{
PendingTexture.CreateUObjects();
// 생성(Create)AsyncEncodeTask
auto AsyncEncodeTask = new (AsyncEncodeTasks)FAsyncEncode<FShadowMapPendingTexture>(&PendingTexture, LightingScenario, Counter, TextureCompressorModule);
// AsyncEncodeTask추가(Add)글로벌 스레드(Thread)실행(Execute).
GThreadPool->AddQueuedWork(AsyncEncodeTask);
}
// 만약 , 이면 현재(Current)스레드(Thread)상태 .
while (Counter.GetValue() > 0)
{
GWarn->UpdateProgress(Counter.GetValue(), PendingTextures.Num());
FPlatformProcess::Sleep(0.0001f);
}
}2.4.2.4 태스크 그래프
TaskGraph를 직역하면 작업 그래프입니다. 사용된 그래프는 DAG(Directed Acylic Graph, 방향성 비순환 그래프)입니다. 종속성을 지정하고 사전 및 사후 주문 작업을 지정할 수 있지만 순환 종속성은 있을 수 없습니다. 이는 지금까지 UE에서 가장 복잡한 병렬 작업 시스템이며, 관련된 개념 및 작동 메커니즘의 복잡성이 급격히 증가했습니다. 이 섹션에서는 메커니즘과 원리를 명확히 하는 것을 목표로 이를 설명하는 데 많은 시간을 할애할 것입니다.
- FBaseGraphTask
FBaseGraphTask는 TaskGraph에서 실행되는 작업입니다. 이는 기본 상위 클래스이며, 이 클래스에서 파생된 특정 작업 하위 클래스만 작업을 실행합니다. 해당 성명(발췌)은 다음과 같습니다.
// Engine\Source\Runtime\Core\Public\Async\TaskGraphInterfaces.h
class FBaseGraphTask
{
protected:
FBaseGraphTask(int32 InNumberOfPrerequistitesOutstanding);
// 또는 .
void PrerequisitesComplete(ENamedThreads::Type CurrentThread, int32 NumAlreadyFinishedPrequistes, bool bUnlock = true);
// 조건 (실행(Execute))실행(Execute)
void ConditionalQueueTask(ENamedThreads::Type CurrentThread)
{
if (NumberOfPrerequistitesOutstanding.Decrement()==0)
{
QueueTask(CurrentThread);
}
}
private:
// 실행(Execute), 에 의해 구현 .
virtual void ExecuteTask(TArray<FBaseGraphTask*>& NewTasks, ENamedThreads::Type CurrentThread)=0;
// 추가(Add)까지 TaskGraph 내에서 .
void QueueTask(ENamedThreads::Type CurrentThreadIfKnown)
{
checkThreadGraph(LifeStage.Increment() == int32(LS_Queued));
FTaskGraphInterface::Get().QueueTask(this, ThreadToExecuteOn, CurrentThreadIfKnown);
}
ENamedThreads::Type ThreadToExecuteOn; // 실행(Execute)의 스레드(Thread)타입/유형
FThreadSafeCounter NumberOfPrerequistitesOutstanding; // 실행(Execute)의
};- TGraphTask
FBaseGraphTask의 유일한 하위 클래스인 TGraphTask는 실행 작업을 완료하기 위해 코드를 인수합니다. TGraphTask의 선언과 구현은 다음과 같습니다.
template<typename TTask>
class TGraphTask final : public FBaseGraphTask
{
public:
// 의 .
class FConstructor
{
public:
// 생성(Create)TTask객체/오브젝트 , 설정(Set)TGraphTask의 데이터 , 로써 실행(Execute).
template<typename...T>
FGraphEventRef ConstructAndDispatchWhenReady(T&&... Args)
{
new ((void *)&Owner->TaskStorage) TTask(Forward<T>(Args)...);
return Owner->Setup(Prerequisites, CurrentThreadIfKnown);
}
// 생성(Create)TTask객체/오브젝트 , 설정(Set)TGraphTask의 데이터 , 그러나 실행(Execute).
template<typename...T>
TGraphTask* ConstructAndHold(T&&... Args)
{
new ((void *)&Owner->TaskStorage) TTask(Forward<T>(Args)...);
return Owner->Hold(Prerequisites, CurrentThreadIfKnown);
}
private:
TGraphTask* Owner; // 의 TGraphTask객체/오브젝트 .
const FGraphEventArray* Prerequisites; // .
ENamedThreads::Type CurrentThreadIfKnown;
};
// 생성(Create), ※ 주의: 반환(Return)의 는 FConstructor객체/오브젝트 , 로써 실행(Execute).
static FConstructor CreateTask(const FGraphEventArray* Prerequisites = NULL, ENamedThreads::Type CurrentThreadIfKnown = ENamedThreads::AnyThread)
{
int32 NumPrereq = Prerequisites ? Prerequisites->Num() : 0;
if (sizeof(TGraphTask) <= FBaseGraphTask::SMALL_TASK_SIZE)
{
void *Mem = FBaseGraphTask::GetSmallTaskAllocator().Allocate();
return FConstructor(new (Mem) TGraphTask(TTask::GetSubsequentsMode() == ESubsequentsMode::FireAndForget ? NULL : FGraphEvent::CreateGraphEvent(), NumPrereq), Prerequisites, CurrentThreadIfKnown);
}
return FConstructor(new TGraphTask(TTask::GetSubsequentsMode() == ESubsequentsMode::FireAndForget ? NULL : FGraphEvent::CreateGraphEvent(), NumPrereq), Prerequisites, CurrentThreadIfKnown);
}
void Unlock(ENamedThreads::Type CurrentThreadIfKnown = ENamedThreads::AnyThread)
{
ConditionalQueueTask(CurrentThreadIfKnown);
}
FGraphEventRef GetCompletionEvent()
{
return Subsequents;
}
private:
// 실행(Execute)
void ExecuteTask(TArray<FBaseGraphTask*>& NewTasks, ENamedThreads::Type CurrentThread) override
{
(......)
// 처리(Process).
if (TTask::GetSubsequentsMode() == ESubsequentsMode::TrackSubsequents)
{
Subsequents->CheckDontCompleteUntilIsEmpty(); // we can only add wait for tasks while executing the task
}
// 실행(Execute)
TTask& Task = *(TTask*)&TaskStorage;
{
FScopeCycleCounter Scope(Task.GetStatId(), true);
Task.DoTask(CurrentThread, Subsequents);
Task.~TTask();
checkThreadGraph(ENamedThreads::GetThreadIndex(CurrentThread) <= ENamedThreads::GetRenderThread() || FMemStack::Get().IsEmpty()); // you must mark and pop memstacks if you use them in tasks! Named threads are excepted.
}
TaskConstructed = false;
// 실행(Execute).
if (TTask::GetSubsequentsMode() == ESubsequentsMode::TrackSubsequents)
{
FPlatformMisc::MemoryBarrier();
Subsequents->DispatchSubsequents(NewTasks, CurrentThread);
}
// 해제(Release)객체/오브젝트 데이터 .
if (sizeof(TGraphTask) <= FBaseGraphTask::SMALL_TASK_SIZE)
{
this->TGraphTask::~TGraphTask();
FBaseGraphTask::GetSmallTaskAllocator().Free(this);
}
else
{
delete this;
}
}
// 설정(Set).
void SetupPrereqs(const FGraphEventArray* Prerequisites, ENamedThreads::Type CurrentThreadIfKnown, bool bUnlock)
{
checkThreadGraph(!TaskConstructed);
TaskConstructed = true;
TTask& Task = *(TTask*)&TaskStorage;
SetThreadToExecuteOn(Task.GetDesiredThread());
int32 AlreadyCompletedPrerequisites = 0;
if (Prerequisites)
{
for (int32 Index = 0; Index < Prerequisites->Num(); Index++)
{
check((*Prerequisites)[Index]);
if (!(*Prerequisites)[Index]->AddSubsequent(this))
{
AlreadyCompletedPrerequisites++;
}
}
}
PrerequisitesComplete(CurrentThreadIfKnown, AlreadyCompletedPrerequisites, bUnlock);
}
// 설정(Set)데이터 .
FGraphEventRef Setup(const FGraphEventArray* Prerequisites = NULL, ENamedThreads::Type CurrentThreadIfKnown = ENamedThreads::AnyThread)
{
FGraphEventRef ReturnedEventRef = Subsequents; // very important so that this doesn't get destroyed before we return
SetupPrereqs(Prerequisites, CurrentThreadIfKnown, true);
return ReturnedEventRef;
}
// 데이터 .
TGraphTask* Hold(const FGraphEventArray* Prerequisites = NULL, ENamedThreads::Type CurrentThreadIfKnown = ENamedThreads::AnyThread)
{
SetupPrereqs(Prerequisites, CurrentThreadIfKnown, false);
return this;
}
// 생성(Create).
static FConstructor CreateTask(FGraphEventRef SubsequentsToAssume, const FGraphEventArray* Prerequisites = NULL, ENamedThreads::Type CurrentThreadIfKnown = ENamedThreads::AnyThread)
{
if (sizeof(TGraphTask) <= FBaseGraphTask::SMALL_TASK_SIZE)
{
void *Mem = FBaseGraphTask::GetSmallTaskAllocator().Allocate();
return FConstructor(new (Mem) TGraphTask(SubsequentsToAssume, Prerequisites ? Prerequisites->Num() : 0), Prerequisites, CurrentThreadIfKnown);
}
return FConstructor(new TGraphTask(SubsequentsToAssume, Prerequisites ? Prerequisites->Num() : 0), Prerequisites, CurrentThreadIfKnown);
}
TAlignedBytes<sizeof(TTask),alignof(TTask)> TaskStorage; // 실행(Execute)의 객체/오브젝트 .
bool TaskConstructed;
FGraphEventRef Subsequents; // 동기화 객체/오브젝트 .
};- TAsyncGraphTask
위에서 볼 수 있듯이 TGraphTask는 작업이지만 실제로 수행하는 작업은 TTask의 템플릿 클래스입니다. TTask의 기본 형식은 UE 주석에 나와 있습니다.
class FGenericTask
{
TSomeType SomeArgument;
public:
FGenericTask(TSomeType InSomeArgument) // 을(를) 활용하여 을(를) 활용하여 , 을(를) 활용하여 .
: SomeArgument(InSomeArgument)
{
// Usually the constructor doesn't do anything except save the arguments for use in DoWork or GetDesiredThread.
}
~FGenericTask()
{
// you will be destroyed immediately after you execute. Might as well do cleanup in DoWork, but you could also use a destructor.
}
FORCEINLINE TStatId GetStatId() const
{
RETURN_QUICK_DECLARE_CYCLE_STAT(FGenericTask, STATGROUP_TaskGraphTasks);
}
[static] ENamedThreads::Type GetDesiredThread()
{
return ENamedThreads::[named thread or AnyThread];
}
void DoTask(ENamedThreads::Type CurrentThread, const FGraphEventRef& MyCompletionGraphEvent)
{
// The arguments are useful for setting up other tasks.
// Do work here, probably using SomeArgument.
MyCompletionGraphEvent->DontCompleteUntil(TGraphTask<FSomeChildTask>::CreateTask(NULL,CurrentThread).ConstructAndDispatchWhenReady());
}
};그러나 자체 작업을 사용자 정의해야 하는 경우 휠을 다시 만들 필요 없이 TAsyncGraphTask 클래스를 직접 사용하거나 파생할 수 있습니다. TAsyncGraphTask 및 해당 상위 클래스 FAsyncGraphTaskBase는 다음과 같이 선언됩니다.
// Engine\Source\Runtime\Core\Public\Async\Async.h
//
namespace ESubsequentsMode
{
enum Type
{
TrackSubsequents, //
FireAndForget // , 로써 스레드(Thread)동기화 , 실행(Execute)효율 .
};
}
class FAsyncGraphTaskBase
{
public:
TStatId GetStatId() const
{
return GET_STATID(STAT_TaskGraph_OtherTasks);
}
// .
static ESubsequentsMode::Type GetSubsequentsMode()
{
return ESubsequentsMode::FireAndForget;
}
};
template<typename ResultType>
class TAsyncGraphTask : public FAsyncGraphTaskBase
{
public:
// , InFunction는 실행(Execute)의 코드 .
TAsyncGraphTask(TUniqueFunction<ResultType()>&& InFunction, TPromise<ResultType>&& InPromise, ENamedThreads::Type InDesiredThread = ENamedThreads::AnyThread)
: Function(MoveTemp(InFunction))
, Promise(MoveTemp(InPromise))
, DesiredThread(InDesiredThread)
{ }
public:
// 실행(Execute)
void DoTask(ENamedThreads::Type CurrentThread, const FGraphEventRef& MyCompletionGraphEvent)
{
SetPromise(Promise, Function);
}
ENamedThreads::Type GetDesiredThread()
{
return DesiredThread;
}
TFuture<ResultType> GetFuture()
{
return Promise.GetFuture();
}
private:
TUniqueFunction<ResultType()> Function; // 실행(Execute)의 함수 객체/오브젝트 .
TPromise<ResultType> Promise; // 동기화 객체/오브젝트 .
ENamedThreads::Type DesiredThread; // 실행(Execute)의 스레드(Thread)타입/유형 .
};- FTaskThreadBase
FTaskThreadBase는 작업 실행을 위한 스레드 상위 클래스입니다. 작업 설정 및 운영을 위한 인터페이스 세트를 정의합니다. 다음과 같이 선언됩니다.
class FTaskThreadBase : public FRunnable, FSingleThreadRunnable
{
public:
FTaskThreadBase()
: ThreadId(ENamedThreads::AnyThread)
, PerThreadIDTLSSlot(0xffffffff)
, OwnerWorker(nullptr)
{
NewTasks.Reset(128);
}
// 설정(Set)데이터 .
void Setup(ENamedThreads::Type InThreadId, uint32 InPerThreadIDTLSSlot, FWorkerThread* InOwnerWorker)
{
ThreadId = InThreadId;
check(ThreadId >= 0);
PerThreadIDTLSSlot = InPerThreadIDTLSSlot;
OwnerWorker = InOwnerWorker;
}
// 로부터 현재(Current)스레드(Thread)초기화(Initialize).
void InitializeForCurrentThread()
{
// 설정(Set)플랫폼의 TLS.
FPlatformTLS::SetTlsValue(PerThreadIDTLSSlot, OwnerWorker);
}
ENamedThreads::Type GetThreadId() const;
// 을(를) 활용하여 의 스레드(Thread)처리(Process)까지 스레드(Thread) 또는 RequestQuit호출(Call).
virtual void ProcessTasksUntilQuit(int32 QueueIndex) = 0;
// 을(를) 활용하여 의 스레드(Thread)처리(Process)까지 스레드(Thread) 또는 RequestQuit호출(Call).
virtual uint64 ProcessTasksUntilIdle(int32 QueueIndex);
// 탈출/종료(Exit). 스레드(Thread)탈출/종료(Exit)까지 호출(Call). 만약 는 의 스레드(Thread), ProcessTasksUntilQuit 내에서 을(를) 활용하여 로써 반환(Return)호출(Call); 스레드(Thread)이면 비활성화(Disable).
virtual void RequestQuit(int32 QueueIndex) = 0;
// , this스레드(Thread) 및 현재(Current)스레드(Thread). 만약 는 의 스레드(Thread), 의 .
virtual void EnqueueFromThisThread(int32 QueueIndex, FBaseGraphTask* Task);
// , this스레드(Thread) 및 현재(Current)스레드(Thread).
virtual bool EnqueueFromOtherThread(int32 QueueIndex, FBaseGraphTask* Task);
// 스레드(Thread).
virtual void WakeUp();
// 는 처리(Process) 내에서 .
virtual bool IsProcessingTasks(int32 QueueIndex) = 0;
// 스레드(Thread)갱신(Update)
virtual void Tick() override
{
ProcessTasksUntilIdle(0);
}
// FRunnable API
virtual bool Init() override
{
InitializeForCurrentThread();
return true;
}
virtual uint32 Run() override
{
check(OwnerWorker); // make sure we are started up
ProcessTasksUntilQuit(0);
FMemory::ClearAndDisableTLSCachesOnCurrentThread();
return 0;
}
virtual void Stop() override
{
RequestQuit(-1);
}
virtual void Exit() override
{
}
virtual FSingleThreadRunnable* GetSingleThreadInterface() override
{
return this;
}
protected:
ENamedThreads::Type ThreadId; // 스레드(Thread)id(스레드(Thread))
uint32 PerThreadIDTLSSlot; // TLS.
FThreadSafeCounter IsStalled; // . 을(를) 활용하여 트리거 신호(Signal).
TArray<FBaseGraphTask*> NewTasks; // 처리(Process)의 리스트 .
FWorkerThread* OwnerWorker; // 의 스레드(Thread)객체/오브젝트 .
};FTaskThreadBase는 추상 클래스일 뿐이며 특정 구현은 FNamedTaskThread 및 FTaskThreadAnyThread 하위 클래스에 의해 완료됩니다.
그중 FNamedTaskThread는 명명된 스레드로 작업을 처리합니다.
// 의 스레드(Thread).
class FNamedTaskThread : public FTaskThreadBase
{
public:
// 을(를) 활용하여 의 스레드(Thread)처리(Process)까지 스레드(Thread) 또는 RequestQuit호출(Call).
virtual void ProcessTasksUntilQuit(int32 QueueIndex) override
{
check(Queue(QueueIndex).StallRestartEvent); // make sure we are started up
Queue(QueueIndex).QuitForReturn = false;
verify(++Queue(QueueIndex).RecursionGuard == 1);
// 루프 처리(Process), 까지 탈출/종료(Exit)、비활성화(Disable) 또는 플랫폼지원 스레드(Thread)。
do
{
ProcessTasksNamedThread(QueueIndex, FPlatformProcess::SupportsMultithreading());
} while (!Queue(QueueIndex).QuitForReturn && !Queue(QueueIndex).QuitForShutdown && FPlatformProcess::SupportsMultithreading()); // @Hack - quit now when running with only one thread.
verify(!--Queue(QueueIndex).RecursionGuard);
}
// 을(를) 활용하여 의 스레드(Thread)처리(Process)까지 스레드(Thread) 또는 RequestQuit호출(Call).
virtual uint64 ProcessTasksUntilIdle(int32 QueueIndex) override
{
check(Queue(QueueIndex).StallRestartEvent); // make sure we are started up
Queue(QueueIndex).QuitForReturn = false;
verify(++Queue(QueueIndex).RecursionGuard == 1);
uint64 ProcessedTasks = ProcessTasksNamedThread(QueueIndex, false);
verify(!--Queue(QueueIndex).RecursionGuard);
return ProcessedTasks;
}
// 처리(Process).
uint64 ProcessTasksNamedThread(int32 QueueIndex, bool bAllowStall)
{
uint64 ProcessedTasks = 0;
(......)
TStatId StallStatId;
bool bCountAsStall = false;
(......)
while (!Queue(QueueIndex).QuitForReturn)
{
// 로부터 페치/가져오기(Fetch).
FBaseGraphTask* Task = Queue(QueueIndex).StallQueue.Pop(0, bAllowStall);
TestRandomizedThreads();
if (!Task)
{
if (bAllowStall)
{
{
FScopeCycleCounter Scope(StallStatId);
Queue(QueueIndex).StallRestartEvent->Wait(MAX_uint32, bCountAsStall);
if (Queue(QueueIndex).QuitForShutdown)
{
return ProcessedTasks;
}
TestRandomizedThreads();
}
continue;
}
else
{
break; // we were asked to quit
}
}
else // 로
{
// 실행(Execute).
Task->Execute(NewTasks, ENamedThreads::Type(ThreadId | (QueueIndex << ENamedThreads::QueueIndexShift)));
ProcessedTasks++;
TestRandomizedThreads();
}
}
return ProcessedTasks;
}
virtual void EnqueueFromThisThread(int32 QueueIndex, FBaseGraphTask* Task) override
{
checkThreadGraph(Task && Queue(QueueIndex).StallRestartEvent); // make sure we are started up
uint32 PriIndex = ENamedThreads::GetTaskPriority(Task->ThreadToExecuteOn) ? 0 : 1;
int32 ThreadToStart = Queue(QueueIndex).StallQueue.Push(Task, PriIndex);
check(ThreadToStart < 0); // if I am stalled, then how can I be queueing a task?
}
virtual void RequestQuit(int32 QueueIndex) override
{
// this will not work under arbitrary circumstances. For example you should not attempt to stop threads unless they are known to be idle.
if (!Queue(0).StallRestartEvent)
{
return;
}
if (QueueIndex == -1)
{
// we are shutting down
checkThreadGraph(Queue(0).StallRestartEvent); // make sure we are started up
checkThreadGraph(Queue(1).StallRestartEvent); // make sure we are started up
Queue(0).QuitForShutdown = true;
Queue(1).QuitForShutdown = true;
Queue(0).StallRestartEvent->Trigger();
Queue(1).StallRestartEvent->Trigger();
}
else
{
checkThreadGraph(Queue(QueueIndex).StallRestartEvent); // make sure we are started up
Queue(QueueIndex).QuitForReturn = true;
}
}
virtual bool EnqueueFromOtherThread(int32 QueueIndex, FBaseGraphTask* Task) override
{
TestRandomizedThreads();
checkThreadGraph(Task && Queue(QueueIndex).StallRestartEvent); // make sure we are started up
uint32 PriIndex = ENamedThreads::GetTaskPriority(Task->ThreadToExecuteOn) ? 0 : 1;
int32 ThreadToStart = Queue(QueueIndex).StallQueue.Push(Task, PriIndex);
if (ThreadToStart >= 0)
{
QUICK_SCOPE_CYCLE_COUNTER(STAT_TaskGraph_EnqueueFromOtherThread_Trigger);
checkThreadGraph(ThreadToStart == 0);
TASKGRAPH_SCOPE_CYCLE_COUNTER(1, STAT_TaskGraph_EnqueueFromOtherThread_Trigger);
Queue(QueueIndex).StallRestartEvent->Trigger();
return true;
}
return false;
}
virtual bool IsProcessingTasks(int32 QueueIndex) override
{
return !!Queue(QueueIndex).RecursionGuard;
}
private:
// 스레드(Thread).
struct FThreadTaskQueue
{
FStallingTaskQueue<FBaseGraphTask, PLATFORM_CACHE_LINE_SIZE, 2> StallQueue; // 의 .
uint32 RecursionGuard; // 루프 ()호출(Call).
bool QuitForReturn; // 는 탈출/종료(Exit).
bool QuitForShutdown; // 는 비활성화(Disable).
FEvent* StallRestartEvent; // 스레드(Thread)의 이벤트 .
};
FORCEINLINE FThreadTaskQueue& Queue(int32 QueueIndex)
{
checkThreadGraph(QueueIndex >= 0 && QueueIndex < ENamedThreads::NumQueues);
return Queues[QueueIndex];
}
FORCEINLINE const FThreadTaskQueue& Queue(int32 QueueIndex) const
{
checkThreadGraph(QueueIndex >= 0 && QueueIndex < ENamedThreads::NumQueues);
return Queues[QueueIndex];
}
FThreadTaskQueue Queues[ENamedThreads::NumQueues]; // 스레드(Thread)을(를) 활용하여 의 .
};FTaskThreadAnyThread는 이름 없는 스레드의 작업을 처리하는 데 사용됩니다. 이름 없는 스레드가 많기 때문에 작업 처리는 FNamedTaskThread와 다릅니다.
class FTaskThreadAnyThread : public FTaskThreadBase
{
public:
virtual void ProcessTasksUntilQuit(int32 QueueIndex) override
{
if (PriorityIndex != (ENamedThreads::BackgroundThreadPriority >> ENamedThreads::ThreadPriorityShift))
{
FMemory::SetupTLSCachesOnCurrentThread();
}
check(!QueueIndex);
do
{
// 처리(Process)
ProcessTasks();
} while (!Queue.QuitForShutdown && FPlatformProcess::SupportsMultithreading()); // @Hack - quit now when running with only one thread.
}
virtual uint64 ProcessTasksUntilIdle(int32 QueueIndex) override
{
if (!FPlatformProcess::SupportsMultithreading())
{
// 처리(Process)
return ProcessTasks();
}
else
{
check(0);
return 0;
}
}
(......)
private:
#if UE_EXTERNAL_PROFILING_ENABLED
static inline const TCHAR* ThreadPriorityToName(int32 PriorityIdx)
{
PriorityIdx <<= ENamedThreads::ThreadPriorityShift;
if (PriorityIdx == ENamedThreads::HighThreadPriority)
{
return TEXT("Task Thread HP"); // 단계 의 스레드(Thread)
}
else if (PriorityIdx == ENamedThreads::NormalThreadPriority)
{
return TEXT("Task Thread NP"); // 단계 의 스레드(Thread)
}
else if (PriorityIdx == ENamedThreads::BackgroundThreadPriority)
{
return TEXT("Task Thread BP"); // 단계 의 스레드(Thread)
}
else
{
return TEXT("Task Thread Unknown Priority");
}
}
#endif
// 의 처리(Process) 및 FNamedTaskThread, 페치/가져오기(Fetch)의 , 는 로부터 TaskGraph 내에서 의 페치/가져오기(Fetch)의 .
uint64 ProcessTasks()
{
LLM_SCOPE(ELLMTag::TaskGraphTasksMisc);
TStatId StallStatId;
bool bCountAsStall = true;
uint64 ProcessedTasks = 0;
(......)
verify(++Queue.RecursionGuard == 1);
bool bDidStall = false;
while (1)
{
// 로부터 TaskGraph 내에서 의 페치/가져오기(Fetch)의 .
FBaseGraphTask* Task = FindWork();
if (!Task)
{
(......)
TestRandomizedThreads();
if (FPlatformProcess::SupportsMultithreading())
{
FScopeCycleCounter Scope(StallStatId);
Queue.StallRestartEvent->Wait(MAX_uint32, bCountAsStall);
bDidStall = true;
}
if (Queue.QuitForShutdown || !FPlatformProcess::SupportsMultithreading())
{
break;
}
TestRandomizedThreads();
(......)
continue;
}
TestRandomizedThreads();
(......)
bDidStall = false;
Task->Execute(NewTasks, ENamedThreads::Type(ThreadId));
ProcessedTasks++;
TestRandomizedThreads();
if (Queue.bStallForTuning)
{
{
FScopeLock Lock(&Queue.StallForTuning);
}
}
}
verify(!--Queue.RecursionGuard);
return ProcessedTasks;
}
// 데이터 .
struct FThreadTaskQueue
{
FEvent* StallRestartEvent;
uint32 RecursionGuard;
bool QuitForShutdown;
bool bStallForTuning;
FCriticalSection StallForTuning; //
};
// 로부터 TaskGraph 내에서 페치/가져오기(Fetch).
FBaseGraphTask* FindWork()
{
return FTaskGraphImplementation::Get().FindWork(ThreadId);
}
FThreadTaskQueue Queue; // , 오직 개 을(를) 활용하여 스레드(Thread).
int32 PriorityIndex;
};- ENamedThreads
TaskGraph의 구현 및 사용을 이해하기 전에 ENamedThreads와 관련된 메커니즘을 이해하는 것이 필요합니다. ENamedThreads는 인코딩 및 디코딩 스레드와 우선 순위 작업을 제공하는 네임스페이스입니다. 선언 및 구문 분석은 다음과 같습니다.
namespace ENamedThreads
{
enum Type : int32
{
UnusedAnchor = -1,
// ----을(를) 활용하여 (의 )스레드(Thread)----
#if STATS
StatsThread, // 스레드(Thread)
#endif
RHIThread, // RHI스레드(Thread)
AudioThread, // 스레드(Thread)
GameThread, // 스레드(Thread)
ActualRenderingThread = GameThread + 1, // 렌더링(Render)스레드(Thread). GetRenderingThread()페치/가져오기(Fetch)의 렌더링(Render)는 렌더링(Render)스레드(Thread)는 스레드(Thread).
AnyThread = 0xff, // 스레드(Thread)(스레드(Thread), 스레드(Thread))
// ---- 및 단계 ----
MainQueue = 0x000, //
LocalQueue = 0x100, // 로컬
NumQueues = 2,
ThreadIndexMask = 0xff,
QueueIndexMask = 0x100,
QueueIndexShift = 8,
// ----、단계 ----
NormalTaskPriority = 0x000, // 단계
HighTaskPriority = 0x200, // 단계
NumTaskPriorities = 2,
TaskPriorityMask = 0x200,
TaskPriorityShift = 9,
// ----스레드(Thread)단계 ----
NormalThreadPriority = 0x000, // 스레드(Thread)단계
HighThreadPriority = 0x400, // 스레드(Thread)단계
BackgroundThreadPriority = 0x800, // 스레드(Thread)단계
NumThreadPriorities = 3,
ThreadPriorityMask = 0xC00,
ThreadPriorityShift = 10,
// 플래그
#if STATS
StatsThread_Local = StatsThread | LocalQueue,
#endif
GameThread_Local = GameThread | LocalQueue,
ActualRenderingThread_Local = ActualRenderingThread | LocalQueue,
AnyHiPriThreadNormalTask = AnyThread | HighThreadPriority | NormalTaskPriority,
AnyHiPriThreadHiPriTask = AnyThread | HighThreadPriority | HighTaskPriority,
AnyNormalThreadNormalTask = AnyThread | NormalThreadPriority | NormalTaskPriority,
AnyNormalThreadHiPriTask = AnyThread | NormalThreadPriority | HighTaskPriority,
AnyBackgroundThreadNormalTask = AnyThread | BackgroundThreadPriority | NormalTaskPriority,
AnyBackgroundHiPriTask = AnyThread | BackgroundThreadPriority | HighTaskPriority,
};
struct FRenderThreadStatics
{
private:
// 저장(Store)렌더링(Render)스레드(Thread),※ 주의: 는 원자적 타입/유형 。
static CORE_API TAtomic<Type> RenderThread;
static CORE_API TAtomic<Type> RenderThread_Local;
};
// ----설정(Set) 및 페치/가져오기(Fetch)렌더링(Render)스레드(Thread)인터페이스 ----
Type GetRenderThread();
Type GetRenderThread_Local();
void SetRenderThread(Type Thread);
void SetRenderThread_Local(Type Thread);
extern CORE_API int32 bHasBackgroundThreads; // 는 스레드(Thread)
extern CORE_API int32 bHasHighPriorityThreads; // 는 단계 스레드(Thread)
// ----설정(Set) 및 페치/가져오기(Fetch)스레드(Thread)、스레드(Thread)단계 、단계 인터페이스 ----
Type GetThreadIndex(Type ThreadAndIndex);
int32 GetQueueIndex(Type ThreadAndIndex);
int32 GetTaskPriority(Type ThreadAndIndex);
int32 GetThreadPriorityIndex(Type ThreadAndIndex);
Type SetPriorities(Type ThreadAndIndex, Type ThreadPriority, Type TaskPriority);
Type SetPriorities(Type ThreadAndIndex, int32 PriorityIndex, bool bHiPri);
Type SetThreadPriority(Type ThreadAndIndex, Type ThreadPriority);
Type SetTaskPriority(Type ThreadAndIndex, Type TaskPriority);
}- FTaskGraph인터페이스
위에서는 많은 작업 유형이 언급되었으며, 이 섹션에는 실제로 이러한 작업의 관리자 및 팩토리 FTaskGraphInterface가 포함됩니다. FTaskGraphInterface는 작업 그래프의 관리자이며 작업 작업 인터페이스를 제공합니다.
class FTaskGraphInterface
{
virtual void QueueTask(class FBaseGraphTask* Task, ENamedThreads::Type ThreadToExecuteOn, ENamedThreads::Type CurrentThreadIfKnown = ENamedThreads::AnyThread) = 0;
public:
// FTaskGraphInterface객체/오브젝트 인터페이스
static CORE_API void Startup(int32 NumThreads);
static CORE_API void Shutdown();
static CORE_API bool IsRunning();
static CORE_API FTaskGraphInterface& Get();
// 스레드(Thread)인터페이스 .
virtual ENamedThreads::Type GetCurrentThreadIfKnown(bool bLocalQueue = false) = 0;
virtual int32 GetNumWorkerThreads() = 0;
virtual bool IsThreadProcessingTasks(ENamedThreads::Type ThreadToCheck) = 0;
virtual void AttachToThread(ENamedThreads::Type CurrentThread)=0;
virtual uint64 ProcessThreadUntilIdle(ENamedThreads::Type CurrentThread)=0;
// 인터페이스 .
virtual void ProcessThreadUntilRequestReturn(ENamedThreads::Type CurrentThread)=0;
virtual void RequestReturn(ENamedThreads::Type CurrentThread)=0;
virtual void WaitUntilTasksComplete(const FGraphEventArray& Tasks, ENamedThreads::Type CurrentThreadIfKnown = ENamedThreads::AnyThread)=0;
virtual void TriggerEventWhenTasksComplete(FEvent* InEvent, const FGraphEventArray& Tasks, ENamedThreads::Type CurrentThreadIfKnown = ENamedThreads::AnyThread, ENamedThreads::Type TriggerThread = ENamedThreads::AnyHiPriThreadHiPriTask)=0;
void WaitUntilTaskCompletes(const FGraphEventRef& Task, ENamedThreads::Type CurrentThreadIfKnown = ENamedThreads::AnyThread);
void TriggerEventWhenTaskCompletes(FEvent* InEvent, const FGraphEventRef& Task, ENamedThreads::Type CurrentThreadIfKnown = ENamedThreads::AnyThread, ENamedThreads::Type TriggerThread = ENamedThreads::AnyHiPriThreadHiPriTask);
virtual void AddShutdownCallback(TFunction<void()>& Callback) = 0;
static void BroadcastSlow_OnlyUseForSpecialPurposes(bool bDoTaskThreads, bool bDoBackgroundThreads, TFunction<void(ENamedThreads::Type CurrentThread)>& Callback);
};FTaskGraphInterface의 구현은 FTaskGraphImplementation 클래스에 있습니다. FTaskGraphImplementation은 특수 스레드 개체인 WorkerThreads(작업자 스레드)를 실행 매체로 사용합니다. 물론 전용(GameThread, RHI, ActualRenderingThread와 같이 이름이 지정된 스레드) 스레드인 경우 전용 작업 대기열에 들어갑니다. 구현 세부 사항이 많기 때문에 나중에 논의하겠습니다.
- FTaskGraph구현
FTaskGraphImplementation은 FTaskGraphInterface의 인터페이스를 상속하고 구현합니다. 일부 인터페이스 및 구현은 다음과 같습니다.
// Engine\Source\Runtime\Core\Private\Async\TaskGraph.cpp
class FTaskGraphImplementation : public FTaskGraphInterface
{
public:
static FTaskGraphImplementation& Get();
// 함수 , 계산/산출(Calculate)스레드(Thread)개수 , 생성(Create)을(를) 활용하여 스레드(Thread) 및 스레드(Thread).
FTaskGraphImplementation(int32)
{
bCreatedHiPriorityThreads = !!ENamedThreads::bHasHighPriorityThreads;
bCreatedBackgroundPriorityThreads = !!ENamedThreads::bHasBackgroundThreads;
int32 MaxTaskThreads = MAX_THREADS; // 스레드(Thread)개수 기본값: 83.
int32 NumTaskThreads = FPlatformMisc::NumberOfWorkerThreadsToSpawn(); // 에 따라 개수 페치/가져오기(Fetch)스레드(Thread)개수 .
// 처리(Process)지원 스레드(Thread)의 플랫폼.
if (!FPlatformProcess::SupportsMultithreading())
{
MaxTaskThreads = 1;
NumTaskThreads = 1;
LastExternalThread = (ENamedThreads::Type)(ENamedThreads::ActualRenderingThread - 1);
bCreatedHiPriorityThreads = false;
bCreatedBackgroundPriorityThreads = false;
ENamedThreads::bHasBackgroundThreads = 0;
ENamedThreads::bHasHighPriorityThreads = 0;
}
else
{
LastExternalThread = ENamedThreads::ActualRenderingThread;
}
// 을(를) 활용하여 스레드(Thread)개수
NumNamedThreads = LastExternalThread + 1;
// 계산/산출(Calculate)스레드(Thread)개수 , 및 는 활성화(Enable)스레드(Thread)단계 、는 생성(Create)단계 스레드(Thread)。
NumTaskThreadSets = 1 + bCreatedHiPriorityThreads + bCreatedBackgroundPriorityThreads;
// 계산/산출(Calculate)의 스레드(Thread)개수 , 83개 .
NumThreads = FMath::Max<int32>(FMath::Min<int32>(NumTaskThreads * NumTaskThreadSets + NumNamedThreads, MAX_THREADS), NumNamedThreads + 1);
NumThreads = FMath::Min(NumThreads, NumNamedThreads + NumTaskThreads * NumTaskThreadSets);
NumTaskThreadsPerSet = (NumThreads - NumNamedThreads) / NumTaskThreadSets;
ReentrancyCheck.Increment(); // just checking for reentrancy
PerThreadIDTLSSlot = FPlatformTLS::AllocTlsSlot();
// 생성(Create)스레드(Thread).
for (int32 ThreadIndex = 0; ThreadIndex < NumThreads; ThreadIndex++)
{
check(!WorkerThreads[ThreadIndex].bAttached); // reentrant?
// 에 따라 는 을(를) 활용하여 스레드(Thread)생성(Create)스레드(Thread).
bool bAnyTaskThread = ThreadIndex >= NumNamedThreads;
if (bAnyTaskThread)
{
WorkerThreads[ThreadIndex].TaskGraphWorker = new FTaskThreadAnyThread(ThreadIndexToPriorityIndex(ThreadIndex));
}
else
{
WorkerThreads[ThreadIndex].TaskGraphWorker = new FNamedTaskThread;
}
WorkerThreads[ThreadIndex].TaskGraphWorker->Setup(ENamedThreads::Type(ThreadIndex), PerThreadIDTLSSlot, &WorkerThreads[ThreadIndex]);
}
TaskGraphImplementationSingleton = this; // this까지 TaskGraphImplementationSingleton, 로써 페치/가져오기(Fetch).
// 설정(Set)스레드(Thread)의 .
for (int32 ThreadIndex = LastExternalThread + 1; ThreadIndex < NumThreads; ThreadIndex++)
{
FString Name;
const ANSICHAR* GroupName = "TaskGraphNormal";
int32 Priority = ThreadIndexToPriorityIndex(ThreadIndex);
EThreadPriority ThreadPri;
uint64 Affinity = FPlatformAffinity::GetTaskGraphThreadMask();
if (Priority == 1)
{
Name = FString::Printf(TEXT("TaskGraphThreadHP %d"), ThreadIndex - (LastExternalThread + 1));
GroupName = "TaskGraphHigh";
ThreadPri = TPri_SlightlyBelowNormal; // we want even hi priority tasks below the normal threads
// If the platform defines FPlatformAffinity::GetTaskGraphHighPriorityTaskMask then use it
if (FPlatformAffinity::GetTaskGraphHighPriorityTaskMask() != 0xFFFFFFFFFFFFFFFF)
{
Affinity = FPlatformAffinity::GetTaskGraphHighPriorityTaskMask();
}
}
else if (Priority == 2)
{
Name = FString::Printf(TEXT("TaskGraphThreadBP %d"), ThreadIndex - (LastExternalThread + 1));
GroupName = "TaskGraphLow";
ThreadPri = TPri_Lowest;
// If the platform defines FPlatformAffinity::GetTaskGraphBackgroundTaskMask then use it
if ( FPlatformAffinity::GetTaskGraphBackgroundTaskMask() != 0xFFFFFFFFFFFFFFFF )
{
Affinity = FPlatformAffinity::GetTaskGraphBackgroundTaskMask();
}
}
else
{
Name = FString::Printf(TEXT("TaskGraphThreadNP %d"), ThreadIndex - (LastExternalThread + 1));
ThreadPri = TPri_BelowNormal; // we want normal tasks below normal threads like the game thread
}
// 계산/산출(Calculate)스레드(Thread)크기 .
#if WITH_EDITOR
uint32 StackSize = 1024 * 1024;
#elif ( UE_BUILD_SHIPPING || UE_BUILD_TEST )
uint32 StackSize = 384 * 1024;
#else
uint32 StackSize = 512 * 1024;
#endif
// 생성(Create)스레드(Thread)의 실행(Execute)스레드(Thread).
WorkerThreads[ThreadIndex].RunnableThread = FRunnableThread::Create(&Thread(ThreadIndex), *Name, StackSize, ThreadPri, Affinity); // these are below normal threads so that they sleep when the named threads are active
WorkerThreads[ThreadIndex].bAttached = true;
if (WorkerThreads[ThreadIndex].RunnableThread)
{
TRACE_SET_THREAD_GROUP(WorkerThreads[ThreadIndex].RunnableThread->GetThreadID(), GroupName);
}
}
}
// .
virtual void QueueTask(FBaseGraphTask* Task, ENamedThreads::Type ThreadToExecuteOn, ENamedThreads::Type InCurrentThreadIfKnown = ENamedThreads::AnyThread) final override
{
TASKGRAPH_SCOPE_CYCLE_COUNTER(2, STAT_TaskGraph_QueueTask);
if (ENamedThreads::GetThreadIndex(ThreadToExecuteOn) == ENamedThreads::AnyThread)
{
TASKGRAPH_SCOPE_CYCLE_COUNTER(3, STAT_TaskGraph_QueueTask_AnyThread);
// 스레드(Thread)지원 의 처리(Process).
if (FPlatformProcess::SupportsMultithreading())
{
// 처리(Process)단계 .
uint32 TaskPriority = ENamedThreads::GetTaskPriority(Task->ThreadToExecuteOn);
int32 Priority = ENamedThreads::GetThreadPriorityIndex(Task->ThreadToExecuteOn);
if (Priority == (ENamedThreads::BackgroundThreadPriority >> ENamedThreads::ThreadPriorityShift) && (!bCreatedBackgroundPriorityThreads || !ENamedThreads::bHasBackgroundThreads))
{
Priority = ENamedThreads::NormalThreadPriority >> ENamedThreads::ThreadPriorityShift; // we don't have background threads, promote to normal
TaskPriority = ENamedThreads::NormalTaskPriority >> ENamedThreads::TaskPriorityShift; // demote to normal task pri
}
else if (Priority == (ENamedThreads::HighThreadPriority >> ENamedThreads::ThreadPriorityShift) && (!bCreatedHiPriorityThreads || !ENamedThreads::bHasHighPriorityThreads))
{
Priority = ENamedThreads::NormalThreadPriority >> ENamedThreads::ThreadPriorityShift; // we don't have hi priority threads, demote to normal
TaskPriority = ENamedThreads::HighTaskPriority >> ENamedThreads::TaskPriorityShift; // promote to hi task pri
}
uint32 PriIndex = TaskPriority ? 0 : 1;
check(Priority >= 0 && Priority < MAX_THREAD_PRIORITIES);
{
TASKGRAPH_SCOPE_CYCLE_COUNTER(4, STAT_TaskGraph_QueueTask_IncomingAnyThreadTasks_Push);
// 실행(Execute), 실행(Execute)실행(Execute)의 ().
int32 IndexToStart = IncomingAnyThreadTasks[Priority].Push(Task, PriIndex);
if (IndexToStart >= 0)
{
StartTaskThread(Priority, IndexToStart);
}
}
return;
}
else
{
ThreadToExecuteOn = ENamedThreads::GameThread;
}
}
// 로써 는 지원 스레드(Thread)의 처리(Process).
ENamedThreads::Type CurrentThreadIfKnown;
if (ENamedThreads::GetThreadIndex(InCurrentThreadIfKnown) == ENamedThreads::AnyThread)
{
CurrentThreadIfKnown = GetCurrentThread();
}
else
{
CurrentThreadIfKnown = ENamedThreads::GetThreadIndex(InCurrentThreadIfKnown);
checkThreadGraph(CurrentThreadIfKnown == ENamedThreads::GetThreadIndex(GetCurrentThread()));
}
{
int32 QueueToExecuteOn = ENamedThreads::GetQueueIndex(ThreadToExecuteOn);
ThreadToExecuteOn = ENamedThreads::GetThreadIndex(ThreadToExecuteOn);
FTaskThreadBase* Target = &Thread(ThreadToExecuteOn);
if (ThreadToExecuteOn == ENamedThreads::GetThreadIndex(CurrentThreadIfKnown))
{
Target->EnqueueFromThisThread(QueueToExecuteOn, Task);
}
else
{
Target->EnqueueFromOtherThread(QueueToExecuteOn, Task);
}
}
}
virtual int32 GetNumWorkerThreads() final override;
virtual ENamedThreads::Type GetCurrentThreadIfKnown(bool bLocalQueue) final override;
virtual bool IsThreadProcessingTasks(ENamedThreads::Type ThreadToCheck) final override;
// 현재(Current)스레드(Thread)까지 Index.
virtual void AttachToThread(ENamedThreads::Type CurrentThread) final override;
// ----처리(Process)인터페이스 ----
virtual uint64 ProcessThreadUntilIdle(ENamedThreads::Type CurrentThread) final override;
virtual void ProcessThreadUntilRequestReturn(ENamedThreads::Type CurrentThread) final override;
virtual void RequestReturn(ENamedThreads::Type CurrentThread) final override;
virtual void WaitUntilTasksComplete(const FGraphEventArray& Tasks, ENamedThreads::Type CurrentThreadIfKnown = ENamedThreads::AnyThread) final override;
virtual void TriggerEventWhenTasksComplete(FEvent* InEvent, const FGraphEventArray& Tasks, ENamedThreads::Type CurrentThreadIfKnown = ENamedThreads::AnyThread, ENamedThreads::Type TriggerThread = ENamedThreads::AnyHiPriThreadHiPriTask) final override;
virtual void AddShutdownCallback(TFunction<void()>& Callback);
// ----스케줄링 인터페이스 ----
// 활성화(Enable)단계 및 의 스레드(Thread).
void StartTaskThread(int32 Priority, int32 IndexToStart);
void StartAllTaskThreads(bool bDoBackgroundThreads);
FBaseGraphTask* FindWork(ENamedThreads::Type ThreadInNeed);
void StallForTuning(int32 Index, bool Stall);
void SetTaskThreadPriorities(EThreadPriority Pri);
private:
// 페치/가져오기(Fetch)의 스레드(Thread)을(를) 활용하여 .
FTaskThreadBase& Thread(int32 Index)
{
checkThreadGraph(Index >= 0 && Index < NumThreads);
checkThreadGraph(WorkerThreads[Index].TaskGraphWorker->GetThreadId() == Index);
return *WorkerThreads[Index].TaskGraphWorker;
}
// 페치/가져오기(Fetch)현재(Current)스레드(Thread).
ENamedThreads::Type GetCurrentThread();
int32 ThreadIndexToPriorityIndex(int32 ThreadIndex);
enum
{
MAX_THREADS = 26 * (CREATE_HIPRI_TASK_THREADS + CREATE_BACKGROUND_TASK_THREADS + 1) + ENamedThreads::ActualRenderingThread + 1,
MAX_THREAD_PRIORITIES = 3
};
FWorkerThread WorkerThreads[MAX_THREADS]; // 스레드(Thread)(스레드(Thread))객체/오브젝트 배열 .
int32 NumThreads; // 을(를) 활용하여 의 스레드(Thread)개수 .
int32 NumNamedThreads; // 을(를) 활용하여 스레드(Thread)개수 .
int32 NumTaskThreadSets;// 스레드(Thread)개수 .
int32 NumTaskThreadsPerSet; // 개 의 스레드(Thread)개수 .
bool bCreatedHiPriorityThreads;
bool bCreatedBackgroundPriorityThreads;
ENamedThreads::Type LastExternalThread;
FThreadSafeCounter ReentrancyCheck;
uint32 PerThreadIDTLSSlot;
TArray<TFunction<void()> > ShutdownCallbacks; // 소멸 의 .
FStallingTaskQueue<FBaseGraphTask, PLATFORM_CACHE_LINE_SIZE, 2> IncomingAnyThreadTasks[MAX_THREAD_PRIORITIES];
};요약하면 TaskGraph는 스레드 우선순위와 백그라운드 스레드 활성화 여부에 따라 다양한 작업자 스레드 세트를 생성한 다음 FWorkerThread 객체를 생성합니다. 작업이 대기열에 추가되면 해당 작업은 작업 목록 IncomingAnyThreadTasks(유형은 스레드 안전 잠금 없는 연결 목록인 FStallingTaskQueue)에 푸시되고 실행 가능한 작업 인덱스가 제거되며 작업 속성(실행될 것으로 예상되는 스레드, 우선 순위 및 작업 인덱스)에 따라 해당 작업자 스레드가 실행되도록 활성화됩니다.
TaskGraph에 포함된 작업자 스레드 FWorkerThread는 다음과 같이 선언됩니다.
struct FWorkerThread
{
FTaskThreadBase* TaskGraphWorker; // 의 FTaskThread객체/오브젝트 (FTaskThread객체/오브젝트 )
FRunnableThread* RunnableThread; // 실행(Execute)의 스레드(Thread).
bool bAttached; // 는 의 스레드(Thread).(을(를) 활용하여 을(를) 활용하여 스레드(Thread))
};TaskGraph는 궁극적으로 FRunnableThread를 사용하여 작업을 수행하는 것을 볼 수 있습니다. TaskGraph 시스템은 마지막으로 FRunnableThread에 연결되어 닫힌 루프를 형성합니다.
이제 TaskGraph 시스템의 백본이 드디어 설명되었습니다. 물론 아직 다루지 않은 기술적인 세부 사항(동기화 이벤트, 트리거 세부 사항, 스케줄링 알고리즘, 잠금 없는 연결 목록 및 일부 개념)이 많이 있습니다. UE 소스 코드를 연구하고 스스로 탐색하는 것은 독자의 몫입니다.
2.5 UE의 멀티스레드 렌더링
이전에 기본적인 준비를 많이 했고, 드디어 본론으로 돌아와서 UE의 멀티스레드 렌더링에 관련된 지식을 이야기했습니다.
2.5.1 UE의 멀티스레드 렌더링 기본
2.5.1.1 주요 장면 유형 및 렌더링 모듈
UE의 장면과 렌더링 모듈에는 많은 개념이 포함됩니다. 주요 유형 및 분석은 다음과 같습니다.
유형 분석하다
유월드 여기에는 서로 상호작용할 수 있는 액터와 구성요소 세트가 포함되어 있습니다. 여러 레벨을 UWorld에 로드하거나 언로드할 수 있습니다. 여러 개의 UWorld 인스턴스가 동시에 존재할 수 있습니다.
U레벨 레벨은 액터와 컴포넌트 세트를 동일한 파일에 저장합니다.
UScene컴포넌트 장면 구성 요소는 조명, 모델, 안개 등과 같이 장면에 추가할 수 있는 모든 개체의 상위 클래스입니다.
UPrimitiveComponent 기본 구성 요소는 렌더링할 수 있거나 물리적 시뮬레이션을 가질 수 있는 모든 객체의 상위 클래스입니다. CPU 레이어 클리핑을 위한 가장 작은 세분성 단위입니다.
ULight컴포넌트 광원 구성 요소는 모든 광원 유형의 상위 클래스입니다.
FS씬 렌더링 모듈에서는 UWorld의 대표자입니다. FScene에 추가된 객체만 렌더러에서 인식됩니다. 렌더링 스레드는 FScene의 모든 상태를 소유합니다(게임 스레드는 이를 직접 수정할 수 없습니다).
FPrimitiveSceneProxy 기본 장면 프록시는 렌더러의 UPrimitiveComponent를 대표하며 렌더링 스레드의 UPrimitiveComponent 상태를 미러링합니다.
FPrimitiveSceneInfo 렌더러의 내부 상태(FRendererModule의 구현을 설명)는 UPrimitiveComponent와 FPrimitiveSceneProxy의 융합과 동일합니다. 렌더러 모듈만 존재하므로 엔진 모듈은 그 존재를 인식할 수 없습니다.
FScenView FScene 내의 단일 뷰를 설명합니다. 동일한 FScene은 여러 보기를 허용합니다. 즉, 여러 뷰로 장면을 그릴 수도 있고, 동시에 여러 뷰를 그릴 수도 있습니다. 프레임마다 새 뷰 인스턴스가 생성됩니다.
F보기정보 뷰는 렌더러 내부에 표시됩니다. 렌더러 모듈만 존재하며, 엔진 모듈은 보이지 않습니다.
FScenViewState 프레임 전체에 걸쳐 액세스해야 하는 뷰에 대한 렌더러 개인 정보를 저장합니다. Game 인스턴스에서 각 ULocalPlayer에는 하나의 FSceneViewState 인스턴스가 있습니다.
FSceneRenderer 프레임마다 생성되어 프레임 사이에 임시 데이터를 캡슐화합니다. 각각 PC와 모바일의 기본 렌더러를 나타내는 FDeferredShadingSceneRenderer(디퍼드 렌더링 씬 렌더러)와 FMobileSceneRenderer(모바일 씬 렌더러)를 파생시킵니다.
2.5.1.2 엔진 모듈 및 렌더링 모듈 대표
명확한 구조를 갖고, 모듈 간의 종속성을 줄이고, 반복 속도를 높이기 위해 UE는 여러 모듈로 나뉩니다. 가장 중요한 것에는 엔진 모듈, 렌더러 모듈, 코어, RHI, 플러그인 등이 포함됩니다. 이전 섹션에서는 많은 개념과 유형을 언급했습니다. 그 중 일부는 엔진 모듈에 존재하고 일부는 렌더러 모듈에 존재합니다. 세부사항은 다음과 같습니다:
엔진 모듈렌더러 모듈
유월드 FScene
UPrimitiveComponent/FPrimitiveSceneProxy FPrimitiveSceneInfo
FSceneView F보기정보
ULocalPlayer FSceneViewState
ULightComponent/FLightSceneProxy FLightSceneInfo
2.5.1.3 게임 스레드 및 렌더링 스레드 표현
게임 스레드 객체는 일반적으로 논리적 업데이트를 거치며 메모리에 지속적인 데이터를 가지고 있습니다. 게임 스레드와 렌더링 스레드 간의 경쟁 조건을 피하기 위해 렌더링 스레드에 추가 메모리 복사본이 저장되고 다른 유형이 사용됩니다. 다음은 UE에 대한 일반적인 유형 매핑 관계입니다(게임 스레드 개체는 U로 시작하고 렌더링 스레드는 F로 시작합니다).
게임 스레드 렌더링 스레드
유월드 FScene
UPrimitiveComponent FPrimitiveSceneProxy/FPrimitiveSceneInfo
- FSceneView/FViewInfo
ULocalPlayer FSceneViewState
ULight컴포넌트 FLightSceneProxy/FLightSceneInfo
게임 스레드 표현은 일반적으로 게임 게임 스레드에 의해 작동되고, 렌더링 스레드 표현은 주로 렌더링 스레드에 의해 작동됩니다. 스레드 간에 데이터를 조작하려고 하면 예측할 수 없는 결과가 발생하고 경쟁 조건이 발생합니다.
/** SceneProxy,스레드(Thread) 내에서 및 데이터 。 */
FStaticMeshSceneProxy::FStaticMeshSceneProxy(UStaticMeshComponent* InComponent):
FPrimitiveSceneProxy(...),
Owner(InComponent->GetOwner()) <======== 此处将AActor指针被缓存
...
/** SceneProxy의 DrawDynamicElements렌더링(Render)렌더링(Render)스레드(Thread) 내에서 호출(Call) */
void FStaticMeshSceneProxy::DrawDynamicElements(...)
{
if (Owner->AnyProperty) <========== 将会引发竞争条件! 游戏线程拥有AActor、UObject的所有状态!!并且UObject对象可能被GC掉,此时再访问会引起程序崩溃!!
}FPrimitiveSceneProxy 및 FLightSceneProxy와 같은 일부 대표는 특별합니다. 이러한 씬 프록시는 엔진 모듈에 속하지만 렌더링 스레드의 배타적인 개체이기도 합니다. 이는 게임 스레드와 렌더링 스레드를 연결하는 다리 역할을 하며 스레드 간 데이터를 전송하는 도구임을 나타냅니다.
2.5.2 UE의 멀티스레드 렌더링 개요
기본적으로 UE에는 게임 스레드(Game Thread), 렌더링 스레드(Render Thread), RHI 스레드(RHI Thread)가 있으며, 모두 전용 스레드(FRunnableThread)에서 독립적으로 실행됩니다.
게임 스레드는 특정 인터페이스를 통해 렌더링 스레드의 대기열에 콜백 인터페이스를 추가하여 나중에 렌더링 스레드가 실행될 때 렌더링 스레드의 대기열에서 콜백을 가져와 하나씩 실행함으로써 명령 목록을 생성합니다.
프런트엔드로 렌더링 스레드에 의해 생성된 명령 목록은 플랫폼 독립적이며 추상 그래픽 API 호출입니다. 백엔드(backtend)인 RHI 스레드는 렌더링 스레드의 명령 목록을 실행하고 지정된 그래픽 API(그래픽 명령이라고 함)에 대한 호출로 변환한 후 실행을 위해 GPU에 제출합니다. 이러한 스레드에서 처리되는 데이터는 일반적으로 서로 다른 프레임입니다. 예를 들어 게임 스레드는 N개의 데이터 프레임을 처리하고, 렌더링 스레드와 RHI 스레드는 N-1개의 데이터 프레임을 처리합니다.

그러나 예외가 있습니다. 예를 들어 렌더링 스레드와 RHI 스레드는 매우 빠르게 실행되며 지연이 거의 없습니다. 이 경우 게임 스레드는 N 프레임을 처리하고, 렌더링 스레드는 N 또는 N-1 프레임을 처리할 수 있으며, RHI 스레드는 N 또는 N-1 프레임을 변환할 수도 있습니다. 그러나 렌더링 스레드는 게임 스레드보다 한 프레임 뒤에 있을 수 없습니다. 그렇지 않으면 렌더링 스레드가 모든 명령을 처리할 때까지 게임 스레드가 중단됩니다.
게다가 렌더링 명령은 병렬로 생성될 수 있으며, RHI 스레드는 다음과 같이 이러한 명령을 병렬로 변환할 수도 있습니다:

UE4는 명령 목록 다이어그램을 병렬로 생성합니다.
멀티스레드 렌더링을 켜면 프레임 속도가 빨라지고 프레임 간 변경 빈도가 줄어드는 이점이 있습니다(프레임 속도가 더 안정적임). Fortnite 모바일 단말기를 예로 들면, RHI 스레드를 켜기 전에 렌더링 스레드가 급격하게 위아래로 변동했습니다. RHI 스레드를 추가한 후 변동이 훨씬 완만해졌고 기본적으로 게임 스레드와 일치했으며 프레임 속도도 많이 향상되었습니다.

2.5.3 게임 스레드 및 렌더링 스레드 구현
2.5.3.1 게임 스레드 구현
게임 스레드는 메인 스레드라고 불리며 엔진 작동의 핵심입니다. 이는 주요 게임 로직과 실행 중인 프로세스 작업을 전달하며, 다른 스레드의 데이터 개시자이기도 합니다.
게임 스레드 생성은 프로그램 항목을 실행하는 스레드입니다. 시스템이 프로세스를 시작할 때 동시에 생성됩니다(프로세스가 작동하려면 최소한 하나의 스레드가 필요하기 때문입니다). 엔진이 시작될 때 전역 변수에 직접 저장되며 나중에 TaskGraph 시스템으로 설정됩니다.
// Engine\Source\Runtime\Launch\Private\LaunchEngineLoop.cpp
int32 FEngineLoop::PreInitPreStartupScreen(const TCHAR* CmdLine)
{
(......)
// 페치/가져오기(Fetch)현재(Current)스레드(Thread)id, 저장(Store)까지 글로벌 변수 내에서 .
GGameThreadId = FPlatformTLS::GetCurrentThreadId();
GIsGameThreadIdInitialized = true;
FPlatformProcess::SetThreadAffinityMask(FPlatformAffinity::GetMainGameMask());
// 설정(Set)스레드(Thread)데이터 (그러나 플랫폼는 의 구현 )
FPlatformProcess::SetupGameThread();
(......)
if (bCreateTaskGraphAndThreadPools)
{
SCOPED_BOOT_TIMING("FTaskGraphInterface::Startup");
FTaskGraphInterface::Startup(FPlatformMisc::NumberOfCores());
// 현재(Current)스레드(Thread)(스레드(Thread))까지 TaskGraph의 GameThread슬롯(Slot) 내에서 . 스레드(Thread) 및 TaskGraph.
FTaskGraphInterface::Get().AttachToThread(ENamedThreads::GameThread);
}
}위의 코드는 또한 다음을 보여줍니다: TaskGraph 시스템의 메인 스레드, 게임 스레드 및 ENamedThreads::GameThread는 실제로 동일한 것이며 모두 동일한 스레드입니다!
위의 초기화 및 설정 후에 다른 곳에서는 TaskGraph 시스템을 통해 작업을 병렬로 처리할 수 있으며, 전역 변수에 액세스하여 게임 스레드가 초기화되었는지, 현재 스레드가 게임 스레드인지 확인할 수도 있습니다.
bool IsInGameThread()
{
return GIsGameThreadIdInitialized && FPlatformTLS::GetCurrentThreadId() == GGameThreadId;
}2.5.3.2 렌더링 스레드 구현
렌더링 스레드는 게임과 다릅니다. 렌더링 명령 및 렌더링 논리 생성을 전담하는 독립적인 스레드입니다. RenderingThread.h는 모든 외부 인터페이스를 선언하며 그 중 일부는 다음과 같습니다.
// Engine\Source\Runtime\RenderCore\Public\RenderingThread.h
// 는 활성화(Enable)의 렌더링(Render)스레드(Thread), 만약 로 false, 이면 렌더링(Render)즉, 실행(Execute), 는 렌더링(Render)커맨드 큐 .
extern RENDERCORE_API bool GIsThreadedRendering;
// 렌더링(Render)스레드(Thread)는 생성(Create). 파라미터 또는 ToggleRenderingThread파라미터 설정(Set).
extern RENDERCORE_API bool GUseThreadedRendering;
// 는 활성화(Enable)RHI스레드(Thread)
extern RENDERCORE_API void SetRHIThreadEnabled(bool bEnableDedicatedThread, bool bEnableRHIOnTaskThreads);
(......)
// 활성화(Enable)렌더링(Render)스레드(Thread).
extern RENDERCORE_API void StartRenderingThread();
// 렌더링(Render)스레드(Thread).
extern RENDERCORE_API void StopRenderingThread();
// 렌더링(Render)스레드(Thread)는 (는 Crash), 만약 crash, 이면 을(를) 활용하여 UE_Log출력.
extern RENDERCORE_API void CheckRenderingThreadHealth();
// 렌더링(Render)스레드(Thread)는 (는 Crash)
extern RENDERCORE_API bool IsRenderingThreadHealthy();
// 추가(Add)개 필수: 개 드로우/렌더(Draw) 또는 flush렌더링(Render)의 .
extern RENDERCORE_API void AddFrameRenderPrerequisite(const FGraphEventRef& TaskToAdd);
// 렌더링(Render), 렌더링(Render).
extern RENDERCORE_API void AdvanceFrameRenderPrerequisite();
// 렌더링(Render)스레드(Thread)의 렌더링(Render)실행(Execute). 스레드(Thread), 오직 스레드(Thread)호출(Call).
extern RENDERCORE_API void FlushRenderingCommands(bool bFlushDeferredDeletes = false);
extern RENDERCORE_API void FlushPendingDeleteRHIResources_GameThread();
extern RENDERCORE_API void FlushPendingDeleteRHIResources_RenderThread();
extern RENDERCORE_API void TickRenderingTickables();
extern RENDERCORE_API void StartRenderCommandFenceBundler();
extern RENDERCORE_API void StopRenderCommandFenceBundler();
(......)RenderingThread.h에는 렌더링 스레드에 대한 렌더링 지침을 대기열에 넣는 데 사용되는 매우 중요한 매크로 ENQUEUE_RENDER_COMMAND도 있습니다. 선언 및 구현은 다음과 같습니다.
// 렌더링(Render)스레드(Thread)렌더링(Render), Type렌더링(Render)의 .
#define ENQUEUE_RENDER_COMMAND(Type) \
struct Type##Name \
{ \
static const char* CStr() { return #Type; } \
static const TCHAR* TStr() { return TEXT(#Type); } \
}; \
EnqueueUniqueRenderCommand<Type##Name>위의 마지막 문장에서는 EnqueueUniqueRenderCommand 명령을 사용하고 계속해서 추적합니다.
// TSTR는 렌더링(Render), LAMBDA는 함수 .
template<typename TSTR, typename LAMBDA>
FORCEINLINE_DEBUGGABLE void EnqueueUniqueRenderCommand(LAMBDA&& Lambda)
{
typedef TEnqueueUniqueRenderCommandType<TSTR, LAMBDA> EURCType;
// 만약 렌더링(Render)스레드(Thread)실행(Execute)렌더링(Render).
if (IsInRenderingThread())
{
FRHICommandListImmediate& RHICmdList = GetImmediateCommandList_ForRenderCommand();
Lambda(RHICmdList);
}
else
{
// 의 렌더링(Render)스레드(Thread)실행(Execute)
if (ShouldExecuteOnRenderThread())
{
CheckNotBlockedOnRenderThread();
// 로부터 GraphTask생성(Create)렌더링(Render).
TGraphTask<EURCType>::CreateTask().ConstructAndDispatchWhenReady(Forward<LAMBDA>(Lambda));
}
else // 의 렌더링(Render)스레드(Thread)실행(Execute), 이면 실행(Execute).
{
EURCType TempCommand(Forward<LAMBDA>(Lambda));
FScopeCycleCounter EURCMacro_Scope(TempCommand.GetStatId());
TempCommand.DoTask(ENamedThreads::GameThread, FGraphEventRef());
}
}
}위의 설명에서는 독립적인 렌더링 스레드가 있는 경우 렌더링 명령은 결국 TaskGraph의 작업 대기열에 대기하고 렌더링 스레드에서 실행될 적절한 시간을 기다립니다. 그중 TEnqueueUniqueRenderCommandType은 렌더링 명령 전용의 특수 TaskGraph 작업 유형입니다. 다음과 같이 선언됩니다.
class RENDERCORE_API FRenderCommand
{
public:
// 렌더링(Render)필수: 렌더링(Render)스레드(Thread)실행(Execute).
static ENamedThreads::Type GetDesiredThread()
{
check(!GIsThreadedRendering || ENamedThreads::GetRenderThread() != ENamedThreads::GameThread);
return ENamedThreads::GetRenderThread();
}
static ESubsequentsMode::Type GetSubsequentsMode()
{
return ESubsequentsMode::FireAndForget;
}
};
template<typename TSTR, typename LAMBDA>
class TEnqueueUniqueRenderCommandType : public FRenderCommand
{
public:
TEnqueueUniqueRenderCommandType(LAMBDA&& InLambda) : Lambda(Forward<LAMBDA>(InLambda)) {}
// 실행(Execute).
void DoTask(ENamedThreads::Type CurrentThread, const FGraphEventRef& MyCompletionGraphEvent)
{
TRACE_CPUPROFILER_EVENT_SCOPE_ON_CHANNEL_STR(TSTR::TStr(), RenderCommandsChannel);
FRHICommandListImmediate& RHICmdList = GetImmediateCommandList_ForRenderCommand();
Lambda(RHICmdList);
}
(......)
private:
LAMBDA Lambda; // 캐싱(Cache)렌더링(Render)함수 .
};대기열에 넣기 렌더링 명령의 작동을 더 잘 이해하기 위해 장면에 조명을 추가하는 구체적인 예를 들어보겠습니다.
void FScene::AddLight(ULightComponent* Light)
{
(......)
// Send a command to the rendering thread to add the light to the scene.
FScene* Scene = this;
FLightSceneInfo* LightSceneInfo = Proxy->LightSceneInfo;
// 렌더링(Render), 로써 렌더링(Render)스레드(Thread)데이터 까지 렌더링(Render).
ENQUEUE_RENDER_COMMAND(FAddLightCommand)(
[Scene, LightSceneInfo](FRHICommandListImmediate& RHICmdList)
{
CSV_SCOPED_TIMING_STAT_EXCLUSIVE(Scene_AddLight);
FScopeCycleCounter Context(LightSceneInfo->Proxy->GetStatId());
Scene->AddLightSceneInfo_RenderThread(LightSceneInfo);
});
}ENQUEUE_RENDER_COMMAND(FAddLightCommand)를 이전에 구문 분석한 매크로 및 템플릿으로 대체하고 확장합니다. 전체 코드는 다음과 같습니다.
struct FAddLightCommandName
{
static const char* CStr() { return "FAddLightCommand"; }
static const TCHAR* TStr() { return TEXT("FAddLightCommand"); }
};
EnqueueUniqueRenderCommand<FAddLightCommandName>(
[Scene, LightSceneInfo](FRHICommandListImmediate& RHICmdList)
{
CSV_SCOPED_TIMING_STAT_EXCLUSIVE(Scene_AddLight);
FScopeCycleCounter Context(LightSceneInfo->Proxy->GetStatId());
Scene->AddLightSceneInfo_RenderThread(LightSceneInfo);
})
{
typedef TEnqueueUniqueRenderCommandType<FAddLightCommandName, LAMBDA> EURCType;
// 만약 렌더링(Render)스레드(Thread)실행(Execute)렌더링(Render).
if (IsInRenderingThread())
{
FRHICommandListImmediate& RHICmdList = GetImmediateCommandList_ForRenderCommand();
Lambda(RHICmdList);
}
else
{
// 의 렌더링(Render)스레드(Thread)실행(Execute)
if (ShouldExecuteOnRenderThread())
{
CheckNotBlockedOnRenderThread();
// 로부터 GraphTask생성(Create)렌더링(Render).
TGraphTask<EURCType>::CreateTask().ConstructAndDispatchWhenReady(Forward<LAMBDA>(Lambda));
}
else // 의 렌더링(Render)스레드(Thread)실행(Execute), 이면 실행(Execute).
{
EURCType TempCommand(Forward<LAMBDA>(Lambda));
FScopeCycleCounter EURCMacro_Scope(TempCommand.GetStatId());
TempCommand.DoTask(ENamedThreads::GameThread, FGraphEventRef());
}
}
}FRenderingThread는 렌더링 스레드의 주요 작업을 수행합니다. 인터페이스 및 구현 코드 중 일부는 다음과 같습니다.
// Engine\Source\Runtime\RenderCore\Private\RenderingThread.cpp
class FRenderingThread : public FRunnable
{
private:
bool bAcquiredThreadOwnership; // 의 RHI스레드(Thread), 렌더링(Render)스레드(Thread)스레드(Thread).
public:
FEvent* TaskGraphBoundSyncEvent; // TaskGraph동기화 이벤트 , 로써 스레드(Thread)을(를) 활용하여 렌더링(Render)스레드(Thread)렌더링(Render)스레드(Thread)바인딩(Bind)까지 TaskGraph 내에서 .
FRenderingThread()
{
bAcquiredThreadOwnership = false;
// 페치/가져오기(Fetch)동기화 이벤트 .
TaskGraphBoundSyncEvent = FPlatformProcess::GetSynchEventFromPool(true);
RHIFlushResources();
}
// FRunnable interface.
virtual bool Init(void) override
{
// 페치/가져오기(Fetch)현재(Current)스레드(Thread)ID까지 글로벌 변수GRenderThreadId, 로써 을(를) 활용하여 .
GRenderThreadId = FPlatformTLS::GetCurrentThreadId();
// 처리(Process)스레드(Thread).
if (!IsRunningRHIInSeparateThread())
{
bAcquiredThreadOwnership = true;
RHIAcquireThreadOwnership();
}
return true;
}
(......)
virtual uint32 Run(void) override
{
// 설정(Set)TLS.
FMemory::SetupTLSCachesOnCurrentThread();
// 설정(Set)렌더링(Render)스레드(Thread)플랫폼의 데이터 .
FPlatformProcess::SetupRenderThread();
(......)
{
// 렌더링(Render)스레드(Thread)루프 .
RenderingThreadMain( TaskGraphBoundSyncEvent );
}
FMemory::ClearAndDisableTLSCachesOnCurrentThread();
return 0;
}
};실행 후 렌더링 스레드 로직에 진입하는 것을 볼 수 있습니다. 여기에 RenderingThreadMain 코드를 입력하여 알아봅니다.
void RenderingThreadMain( FEvent* TaskGraphBoundSyncEvent )
{
LLM_SCOPE(ELLMTag::RenderingThreadMemory);
// 렌더링(Render)스레드(Thread) 및 로컬 스레드(Thread)스레드(Thread)슬롯(Slot)설정(Set)ActualRenderingThread 및 ActualRenderingThread_Local.
ENamedThreads::Type RenderThread = ENamedThreads::Type(ENamedThreads::ActualRenderingThread);
ENamedThreads::SetRenderThread(RenderThread);
ENamedThreads::SetRenderThread_Local(ENamedThreads::Type(ENamedThreads::ActualRenderingThread_Local));
// 현재(Current)스레드(Thread)까지 TaskGraph의 RenderThread슬롯(Slot) 내에서 .
FTaskGraphInterface::Get().AttachToThread(RenderThread);
FPlatformMisc::MemoryBarrier();
// 트리거 동기화 이벤트 , 스레드(Thread)렌더링(Render)스레드(Thread)까지 TaskGraph, .
if( TaskGraphBoundSyncEvent != NULL )
{
TaskGraphBoundSyncEvent->Trigger();
}
(......)
// 렌더링(Render)스레드(Thread)스테이지 의 처리(Process).
FCoreDelegates::PostRenderingThreadCreated.Broadcast();
check(GIsThreadedRendering);
FTaskGraphInterface::Get().ProcessThreadUntilRequestReturn(RenderThread);
FPlatformMisc::MemoryBarrier();
check(!GIsThreadedRendering);
FCoreDelegates::PreRenderingThreadDestroyed.Broadcast();
(......)
// 복구 스레드(Thread)스레드(Thread)까지 스레드(Thread).
ENamedThreads::SetRenderThread(ENamedThreads::GameThread);
ENamedThreads::SetRenderThread_Local(ENamedThreads::GameThread_Local);
FPlatformMisc::MemoryBarrier();
}그러나 여기에는 여전히 큰 질문이 있습니다. 즉, FRenderingThread는 현재 스레드를 렌더링 스레드로 가져와 이를 TaskGraph에 연결만 하고 스레드를 생성하지 않는다는 것입니다. 그렇다면 렌더링 스레드는 어디에서 생성됩니까? 계속 추적해 보면 FRenderingThread 인스턴스가 StartRenderingThread() 인터페이스에서 생성된 것으로 나타났습니다. 구현 코드는 다음과 같습니다(발췌).
// Engine\Source\Runtime\RenderCore\Private\RenderingThread.cpp
void StartRenderingThread()
{
(......)
// Turn on the threaded rendering flag.
GIsThreadedRendering = true;
// 생성(Create)FRenderingThread인스턴스 .
GRenderingThreadRunnable = new FRenderingThread();
// 생성(Create)렌더링(Render)스레드(Thread)!!
GRenderingThread = FRunnableThread::Create(GRenderingThreadRunnable, *BuildRenderingThreadName(ThreadCount), 0, FPlatformAffinity::GetRenderingThreadPriority(), FPlatformAffinity::GetRenderingThreadMask(), FPlatformAffinity::GetRenderingThreadFlags());
(......)
// 활성화(Enable)렌더링(Render)의 배리어/펜스 .
FRenderCommandFence Fence;
Fence.BeginFence();
Fence.Wait();
(......)
}계속 추적하면 FEngineLoop::PreInitPostStartupScreen에서 StartRenderingThread()가 호출되는 것을 확인할 수 있습니다.
이 시점에서는 렌더링 스레드의 기본 인터페이스 생성, 초기화 및 구현이 분석되었습니다.
2.5.3.3 RHI 스레드 구현
RHI 스레드의 작업은 렌더링 지침을 지정된 그래픽 API로 변환하고 렌더링 리소스를 생성하여 GPU에 업로드하는 것입니다. 주요 로직은 FRHIThread에 있으며 구현 코드는 다음과 같습니다.
// Engine\Source\Runtime\RenderCore\Private\RenderingThread.cpp
class FRHIThread : public FRunnable
{
public:
FRunnableThread* Thread; // 의 RHI스레드(Thread).
FRHIThread()
: Thread(nullptr)
{
check(IsInGameThread());
}
void Start()
{
// 생성(Create)RHI스레드(Thread).
Thread = FRunnableThread::Create(this, TEXT("RHIThread"), 512 * 1024, FPlatformAffinity::GetRHIThreadPriority(),
FPlatformAffinity::GetRHIThreadMask(), FPlatformAffinity::GetRHIThreadFlags()
);
check(Thread);
}
virtual uint32 Run() override
{
LLM_SCOPE(ELLMTag::RHIMisc);
// 초기화(Initialize)TLS
FMemory::SetupTLSCachesOnCurrentThread();
// FRHIThread의 RHI스레드(Thread)까지 askGraph 내에서 ,까지 ENamedThreads::RHIThread。
FTaskGraphInterface::Get().AttachToThread(ENamedThreads::RHIThread);
// RHI스레드(Thread),까지 스레드(Thread)반환(Return)。
FTaskGraphInterface::Get().ProcessThreadUntilRequestReturn(ENamedThreads::RHIThread);
// TLS.
FMemory::ClearAndDisableTLSCachesOnCurrentThread();
return 0;
}
// 인터페이스 。
static FRHIThread& Get()
{
static FRHIThread Singleton; // 을(를) 활용하여 로컬 정적 변수,로써 스레드(Thread)。
return Singleton;
}
};RHI 스레드는 렌더링 스레드와 다르며 실제 스레드는 FRHIThread 객체 내에서 직접 생성되는 것을 볼 수 있습니다. FRHIThread 생성은 StartRenderingThread()에서도 이루어집니다.
void StartRenderingThread()
{
(......)
if (GUseRHIThread_InternalUseOnly)
{
FRHICommandListExecutor::GetImmediateCommandList().ImmediateFlush(EImmediateFlushType::DispatchToRHIThread);
if (!FTaskGraphInterface::Get().IsThreadProcessingTasks(ENamedThreads::RHIThread))
{
// 생성(Create)FRHIThread인스턴스 .
FRHIThread::Get().Start();
}
DECLARE_CYCLE_STAT(TEXT("Wait For RHIThread"), STAT_WaitForRHIThread, STATGROUP_TaskGraphTasks);
// 생성(Create)RHI스레드(Thread), 스레드(Thread).
FGraphEventRef CompletionEvent = TGraphTask<FOwnershipOfRHIThreadTask>::CreateTask(NULL, ENamedThreads::GameThread).ConstructAndDispatchWhenReady(true, GET_STATID(STAT_WaitForRHIThread));
QUICK_SCOPE_CYCLE_COUNTER(STAT_StartRenderingThread);
// 스레드(Thread) 또는 로컬 스레드(Thread)RHI스레드(Thread)처리(Process)(스레드(Thread), API로 ).
FTaskGraphInterface::Get().WaitUntilTaskCompletes(CompletionEvent, ENamedThreads::GameThread_Local);
// 저장(Store)RHI스레드(Thread)id.
GRHIThread_InternalUseOnly = FRHIThread::Get().Thread;
check(GRHIThread_InternalUseOnly);
GIsRunningRHIInDedicatedThread_InternalUseOnly = true;
GIsRunningRHIInSeparateThread_InternalUseOnly = true;
GRHIThreadId = GRHIThread_InternalUseOnly->GetThreadID();
GRHICommandList.LatchBypass();
}
(......)
}그렇다면 렌더링 스레드는 어떻게 작업을 RHI 스레드에 대기열에 추가합니까? 대답은 RHICommandList.h에 있습니다:
// Engine\Source\Runtime\RHI\Public\RHICommandList.h
// RHI
struct FRHICommandBase
{
FRHICommandBase* Next = nullptr; // 개 RHI.
// 실행(Execute)RHI소멸 .
virtual void ExecuteAndDestruct(FRHICommandListBase& CmdList, FRHICommandListDebugContext& DebugContext) = 0;
};
// RHI구조체
template<typename TCmd, typename NameType = FUnnamedRhiCommand>
struct FRHICommand : public FRHICommandBase
{
(......)
void ExecuteAndDestruct(FRHICommandListBase& CmdList, FRHICommandListDebugContext& Context) override final
{
(......)
TCmd *ThisCmd = static_cast<TCmd*>(this);
ThisCmd->Execute(CmdList);
ThisCmd->~TCmd();
}
};
// RHI스레드(Thread)RHI의 .
#define FRHICOMMAND_MACRO(CommandName) \
struct PREPROCESSOR_JOIN(CommandName##String, __LINE__) \
{ \
static const TCHAR* TStr() { return TEXT(#CommandName); } \
}; \
struct CommandName final : public FRHICommand<CommandName, PREPROCESSOR_JOIN(CommandName##String, __LINE__)>RHI 스레드의 관련 구현 메커니즘과 렌더링 스레드 유형이 더 간결해졌습니다. 다음은 그 사용법을 보여주는 것입니다:
// Engine\Source\Runtime\RHI\Public\RHICommandList.h
FRHICOMMAND_MACRO(FRHICommandDrawPrimitive)
{
uint32 BaseVertexIndex;
uint32 NumPrimitives;
uint32 NumInstances;
FORCEINLINE_DEBUGGABLE FRHICommandDrawPrimitive(uint32 InBaseVertexIndex, uint32 InNumPrimitives, uint32 InNumInstances)
: BaseVertexIndex(InBaseVertexIndex)
, NumPrimitives(InNumPrimitives)
, NumInstances(InNumInstances)
{
}
RHI_API void Execute(FRHICommandListBase& CmdList);
};
// Engine\Source\Runtime\RHI\Public\RHICommandListCommandExecutes.inl
void FRHICommandDrawPrimitive::Execute(FRHICommandListBase& CmdList)
{
RHISTAT(DrawPrimitive);
INTERNAL_DECORATOR(RHIDrawPrimitive)(BaseVertexIndex, NumPrimitives, NumInstances);
}모든 RHI 명령어는 미리 선언되어 구현되어 있는 것을 볼 수 있습니다. 현재 거의 100가지 유형의 RHI 렌더링 명령이 있습니다(아래 참조). 렌더링 스레드에 의해 생성된 이러한 선언된 RHI 명령은 적절한 시간에 RHI 스레드 큐에 푸시되어 실행될 수 있습니다.
FRHICOMMAND_MACRO(FRHICommandUpdateGeometryCacheBuffer)
FRHICOMMAND_MACRO(FRHISubmitFrameToEncoder)
FRHICOMMAND_MACRO(FLocalRHICommand)
FRHICOMMAND_MACRO(FRHISetSpectatorScreenTexture)
FRHICOMMAND_MACRO(FRHISetSpectatorScreenModeTexturePlusEyeLayout)
FRHICOMMAND_MACRO(FRHISyncFrameCommand)
FRHICOMMAND_MACRO(FRHICommandStat)
FRHICOMMAND_MACRO(FRHICommandRHIThreadFence)
FRHICOMMAND_MACRO(FRHIAsyncComputeSubmitList)
FRHICOMMAND_MACRO(FRHICommandWaitForAndSubmitSubListParallel)
FRHICOMMAND_MACRO(FRHICommandWaitForAndSubmitSubList)
FRHICOMMAND_MACRO(FRHICommandWaitForAndSubmitRTSubList)
FRHICOMMAND_MACRO(FRHICommandSubmitSubList)
FRHICOMMAND_MACRO(FRHICommandBeginUpdateMultiFrameResource)
FRHICOMMAND_MACRO(FRHICommandEndUpdateMultiFrameResource)
FRHICOMMAND_MACRO(FRHICommandBeginUpdateMultiFrameUAV)
FRHICOMMAND_MACRO(FRHICommandEndUpdateMultiFrameUAV)
FRHICOMMAND_MACRO(FRHICommandSetGPUMask)
FRHICOMMAND_MACRO(FRHICommandWaitForTemporalEffect)
FRHICOMMAND_MACRO(FRHICommandBroadcastTemporalEffect)
FRHICOMMAND_MACRO(FRHICommandSetStencilRef)
FRHICOMMAND_MACRO(FRHICommandDrawPrimitive)
FRHICOMMAND_MACRO(FRHICommandDrawIndexedPrimitive)
FRHICOMMAND_MACRO(FRHICommandSetBlendFactor)
FRHICOMMAND_MACRO(FRHICommandSetStreamSource)
FRHICOMMAND_MACRO(FRHICommandSetViewport)
FRHICOMMAND_MACRO(FRHICommandSetStereoViewport)
FRHICOMMAND_MACRO(FRHICommandSetScissorRect)
FRHICOMMAND_MACRO(FRHICommandSetRenderTargets)
FRHICOMMAND_MACRO(FRHICommandBeginRenderPass)
FRHICOMMAND_MACRO(FRHICommandEndRenderPass)
FRHICOMMAND_MACRO(FRHICommandNextSubpass)
FRHICOMMAND_MACRO(FRHICommandBeginParallelRenderPass)
FRHICOMMAND_MACRO(FRHICommandEndParallelRenderPass)
FRHICOMMAND_MACRO(FRHICommandBeginRenderSubPass)
FRHICOMMAND_MACRO(FRHICommandEndRenderSubPass)
FRHICOMMAND_MACRO(FRHICommandBeginComputePass)
FRHICOMMAND_MACRO(FRHICommandEndComputePass)
FRHICOMMAND_MACRO(FRHICommandBindClearMRTValues)
FRHICOMMAND_MACRO(FRHICommandSetGraphicsPipelineState)
FRHICOMMAND_MACRO(FRHICommandAutomaticCacheFlushAfterComputeShader)
FRHICOMMAND_MACRO(FRHICommandFlushComputeShaderCache)
FRHICOMMAND_MACRO(FRHICommandDrawPrimitiveIndirect)
FRHICOMMAND_MACRO(FRHICommandDrawIndexedIndirect)
FRHICOMMAND_MACRO(FRHICommandDrawIndexedPrimitiveIndirect)
FRHICOMMAND_MACRO(FRHICommandSetDepthBounds)
FRHICOMMAND_MACRO(FRHICommandClearUAVFloat)
FRHICOMMAND_MACRO(FRHICommandClearUAVUint)
FRHICOMMAND_MACRO(FRHICommandCopyToResolveTarget)
FRHICOMMAND_MACRO(FRHICommandCopyTexture)
FRHICOMMAND_MACRO(FRHICommandResummarizeHTile)
FRHICOMMAND_MACRO(FRHICommandTransitionTexturesDepth)
FRHICOMMAND_MACRO(FRHICommandTransitionTextures)
FRHICOMMAND_MACRO(FRHICommandTransitionTexturesArray)
FRHICOMMAND_MACRO(FRHICommandTransitionTexturesPipeline)
FRHICOMMAND_MACRO(FRHICommandTransitionTexturesArrayPipeline)
FRHICOMMAND_MACRO(FRHICommandClearColorTexture)
FRHICOMMAND_MACRO(FRHICommandClearDepthStencilTexture)
FRHICOMMAND_MACRO(FRHICommandClearColorTextures)
FRHICOMMAND_MACRO(FRHICommandSetGlobalUniformBuffers)
FRHICOMMAND_MACRO(FRHICommandBuildLocalUniformBuffer)
FRHICOMMAND_MACRO(FRHICommandBeginRenderQuery)
FRHICOMMAND_MACRO(FRHICommandEndRenderQuery)
FRHICOMMAND_MACRO(FRHICommandCalibrateTimers)
FRHICOMMAND_MACRO(FRHICommandPollOcclusionQueries)
FRHICOMMAND_MACRO(FRHICommandBeginScene)
FRHICOMMAND_MACRO(FRHICommandEndScene)
FRHICOMMAND_MACRO(FRHICommandBeginFrame)
FRHICOMMAND_MACRO(FRHICommandEndFrame)
FRHICOMMAND_MACRO(FRHICommandBeginDrawingViewport)
FRHICOMMAND_MACRO(FRHICommandEndDrawingViewport)
FRHICOMMAND_MACRO(FRHICommandInvalidateCachedState)
FRHICOMMAND_MACRO(FRHICommandDiscardRenderTargets)
FRHICOMMAND_MACRO(FRHICommandDebugBreak)
FRHICOMMAND_MACRO(FRHICommandUpdateTextureReference)
FRHICOMMAND_MACRO(FRHICommandUpdateRHIResources)
FRHICOMMAND_MACRO(FRHICommandCopyBufferRegion)
FRHICOMMAND_MACRO(FRHICommandCopyBufferRegions)
FRHICOMMAND_MACRO(FRHICommandClearRayTracingBindings)
FRHICOMMAND_MACRO(FRHICommandRayTraceOcclusion)
FRHICOMMAND_MACRO(FRHICommandRayTraceIntersection)
FRHICOMMAND_MACRO(FRHICommandRayTraceDispatch)
FRHICOMMAND_MACRO(FRHICommandSetRayTracingBindings)
FRHICOMMAND_MACRO(FClearCachedRenderingDataCommand)
FRHICOMMAND_MACRO(FClearCachedElementDataCommand)2.5.4 게임 스레드와 렌더링 스레드 간의 상호 작용
이 섹션에서는 각 스레드 간의 데이터 교환 메커니즘과 구현 세부 사항을 설명합니다. 먼저 게임 스레드가 렌더링 스레드에 데이터를 전달하는 방법을 살펴보세요.
게임 스레드가 Tick이면 UGameEngine, FViewport, UGameViewportClient 및 기타 객체를 통해 렌더링 모듈 호출에 들어갑니다.
void UGameEngine::Tick( float DeltaSeconds, bool bIdleMode )
{
UGameEngine::RedrawViewports()
{
void FViewport::Draw( bool bShouldPresent)
{
void UGameViewportClient::Draw()
{
// 계산/산출(Calculate)ViewFamily、View의
ULocalPlayer::CalcSceneView();
// 렌더링(Render)
FRendererModule::BeginRenderingViewFamily()
{
World->SendAllEndOfFrameUpdates();
// 생성(Create)렌더링(Render)
FSceneRenderer* SceneRenderer = FSceneRenderer::CreateSceneRenderer(ViewFamily, ...);
// 렌더링(Render)스레드(Thread)드로우/렌더(Draw).
ENQUEUE_RENDER_COMMAND(FDrawSceneCommand)(
[SceneRenderer](FRHICommandListImmediate& RHICmdList)
{
RenderViewFamily_RenderThread(RHICmdList, SceneRenderer)
{
(......)
// 호출(Call)렌더링(Render)의 드로우/렌더(Draw)인터페이스 .
SceneRenderer->Render(RHICmdList);
(......)
}
FlushPendingDeleteRHIResources_RenderThread();
});
}
}}}}이전 장에서 언급했듯이 렌더링 스레드는 SceneProxy 및 SceneInfo와 같은 개체를 사용합니다. 그렇다면 게임의 Actor 구성 요소는 장면 프록시 데이터에 어떻게 연결됩니까? 데이터는 어떻게 업데이트되나요?
먼저 SceneProxy로 데이터를 전송하는 게임 구성 요소의 메커니즘을 파악합니다. 답은 FScene::AddPrimitive에 있습니다:
// Engine\Source\Runtime\Renderer\Private\RendererScene.cpp
void FScene::AddPrimitive(UPrimitiveComponent* Primitive)
{
(......)
// 생성(Create)의
FPrimitiveSceneProxy* PrimitiveSceneProxy = Primitive->CreateSceneProxy();
Primitive->SceneProxy = PrimitiveSceneProxy;
if(!PrimitiveSceneProxy)
{
return;
}
// 생성(Create)의 정보
FPrimitiveSceneInfo* PrimitiveSceneInfo = new FPrimitiveSceneInfo(Primitive, this);
PrimitiveSceneProxy->PrimitiveSceneInfo = PrimitiveSceneInfo;
(......)
FScene* Scene = this;
ENQUEUE_RENDER_COMMAND(AddPrimitiveCommand)(
[Params = MoveTemp(Params), Scene, PrimitiveSceneInfo, PreviousTransform = MoveTemp(PreviousTransform)](FRHICommandListImmediate& RHICmdList)
{
FPrimitiveSceneProxy* SceneProxy = Params.PrimitiveSceneProxy;
(......)
SceneProxy->CreateRenderThreadResources();
// 렌더링(Render)스레드(Thread) 내에서 SceneInfo추가(Add)까지 내에서 .
Scene->AddPrimitiveSceneInfo_RenderThread(PrimitiveSceneInfo, PreviousTransform);
});
}위에 핵심 문장이 있습니다: Primitive->CreateSceneProxy()는 컴포넌트에 해당하는 PrimitiveSceneProxy를 생성하는 것입니다. PrimitiveSceneProxy의 생성자에서 구성 요소의 모든 데이터가 복사됩니다.
FPrimitiveSceneProxy::FPrimitiveSceneProxy(const UPrimitiveComponent* InComponent, FName InResourceName)
:
CustomPrimitiveData(InComponent->GetCustomPrimitiveData())
, TranslucencySortPriority(FMath::Clamp(InComponent->TranslucencySortPriority, SHRT_MIN, SHRT_MAX))
, Mobility(InComponent->Mobility)
, LightmapType(InComponent->LightmapType)
, StatId()
, DrawInGame(InComponent->IsVisible())
, DrawInEditor(InComponent->GetVisibleFlag())
, bReceivesDecals(InComponent->bReceivesDecals)
(......)
{
(......)
}데이터를 복사한 후 게임 스레드는 PrimitiveComponent의 데이터를 수정하고, 렌더링 스레드는 PrimitiveSceneProxy의 데이터를 수정하거나 액세스합니다. 서로 간섭하지 않고 중요한 섹션과 잠금의 동기화를 피하고 스레드 안전성을 보장합니다. 그러나 여기에는 여전히 질문이 있습니다. 즉, PrimitiveSceneProxy를 생성할 때 데이터 복사본이 복사되지만 생성 후 PrimitiveComponent는 어떻게 데이터를 PrimitiveSceneProxy로 업데이트합니까?
ActorComponent에는 여러 개의 태그가 있는 것으로 나타났습니다. 이러한 태그가 true로 표시되어 있으면 업데이트를 가져오기 위해 적절한 시간에 업데이트 인터페이스가 호출됩니다.
// Engine\Source\Runtime\Engine\Classes\Components\ActorComponent.h
class ENGINE_API UActorComponent : public UObject, public IInterface_AssetUserData
{
protected:
// 로써 인터페이스 갱신(Update)의 상태 , 로써 로써 구현 의 갱신(Update)로직 .
virtual void DoDeferredRenderUpdates_Concurrent()
{
(......)
if(bRenderStateDirty)
{
RecreateRenderState_Concurrent();
}
else
{
if(bRenderTransformDirty)
{
SendRenderTransform_Concurrent();
}
if(bRenderDynamicDataDirty)
{
SendRenderDynamicData_Concurrent();
}
}
}
virtual void CreateRenderState_Concurrent(FRegisterComponentContext* Context)
{
bRenderStateCreated = true;
bRenderStateDirty = false;
bRenderTransformDirty = false;
bRenderDynamicDataDirty = false;
}
virtual void SendRenderTransform_Concurrent()
{
bRenderTransformDirty = false;
}
virtual void SendRenderDynamicData_Concurrent()
{
bRenderDynamicDataDirty = false;
}
private:
uint8 bRenderStateDirty:1; // 의 렌더링(Render)상태 는 의
uint8 bRenderTransformDirty:1; // 의 변환(Transform)모멘트(Moment)는 의
uint8 bRenderDynamicDataDirty:1; // 의 렌더링(Render)동적 데이터 는 의
};위의 보호된 인터페이스는 해당 SceneProxy에 대한 구성 요소 데이터를 새로 고치는 데 사용됩니다. 특정 구성 요소 하위 클래스는 이를 다시 작성하여 자체 업데이트 논리를 사용자 정의할 수 있습니다. 예를 들어, ULightComponent의 변환 행렬 업데이트 로직은 다음과 같습니다:
// Engine\Source\Runtime\Engine\Private\Components\LightComponent.cpp
void ULightComponent::SendRenderTransform_Concurrent()
{
// 변환(Transform)정보 갱신(Update)까지 .
GetWorld()->Scene->UpdateLightTransform(this);
Super::SendRenderTransform_Concurrent();
}장면의 UpdateLightTransform은 구성 요소의 데이터를 조합하고 실행을 위해 렌더링 스레드로 데이터를 보냅니다.
// Engine\Source\Runtime\Renderer\Private\RendererScene.cpp
void FScene::UpdateLightTransform(ULightComponent* Light)
{
if(Light->SceneProxy)
{
// 의 데이터 까지 구조체 (※ 주의: Component의 까지 렌더링(Render)스레드(Thread),는 갱신(Update)의 데이터 )
FUpdateLightTransformParameters Parameters;
Parameters.LightToWorld = Light->GetComponentTransform().ToMatrixNoScale();
Parameters.Position = Light->GetLightPosition();
FScene* Scene = this;
FLightSceneInfo* LightSceneInfo = Light->SceneProxy->GetLightSceneInfo();
// 데이터 까지 렌더링(Render)스레드(Thread)실행(Execute).
ENQUEUE_RENDER_COMMAND(UpdateLightTransform)(
[Scene, LightSceneInfo, Parameters](FRHICommandListImmediate& RHICmdList)
{
FScopeCycleCounter Context(LightSceneInfo->Proxy->GetStatId());
// 렌더링(Render)스레드(Thread)실행(Execute)데이터 갱신(Update).
Scene->UpdateLightTransform_RenderThread(LightSceneInfo, Parameters);
});
}
}
void FScene::UpdateLightTransform_RenderThread(FLightSceneInfo* LightSceneInfo, const FUpdateLightTransformParameters& Parameters)
{
(......)
// 갱신(Update)변환(Transform)모멘트(Moment).
LightSceneInfo->Proxy->SetTransform(Parameters.LightToWorld, Parameters.Position);
(......)
}이 시점에서 구성 요소가 장면 프록시에 데이터를 업데이트하는 방법에 대한 논리가 최종적으로 명확해졌습니다.
FScene 및 FSceneProxy와 같은 일부 인터페이스는 게임 스레드에서 호출되는 반면, 일부 인터페이스(일반적으로 접미사 _RenderThread 포함)는 렌더링 스레드에서 호출된다는 점을 특별히 기억해 두십시오. 스레드 간에 호출하지 마십시오. 그렇지 않으면 경쟁 조건이 발생하고 프로그램도 충돌할 수 있습니다.
2.5.5 게임 스레드와 렌더링 스레드의 동기화
앞서 언급했듯이 게임 스레드는 렌더링 스레드보다 한 프레임 이상 앞서 있을 수 없습니다. 그렇지 않으면 게임 스레드는 렌더링 스레드가 처리를 마칠 때까지 기다립니다. 동기화 메커니즘에는 두 가지 주요 개념이 포함됩니다.
// Engine\Source\Runtime\RenderCore\Public\RenderCommandFence.h
// 렌더링(Render)배리어/펜스
class RENDERCORE_API FRenderCommandFence
{
public:
// 렌더링(Render)커맨드 큐 추가(Add)개 배리어/펜스 . bSyncToRHIAndGPU는 동기화 RHI 및 GPUBuffer, 이면 오직 렌더링(Render)스레드(Thread).
void BeginFence(bool bSyncToRHIAndGPU = false);
// 배리어/펜스 실행(Execute). bProcessGameThreadTasks을(를) 활용하여 .
void Wait(bool bProcessGameThreadTasks = false) const;
// 는 배리어/펜스 .
bool IsFenceComplete() const;
private:
mutable FGraphEventRef CompletionEvent; // 처리(Process)동기화 의 이벤트
ENamedThreads::Type TriggerThreadIndex; // 처리(Process)트리거 의 스레드(Thread)타입/유형 .
};
// Engine\Source\Runtime\Engine\Public\UnrealEngine.h
class FFrameEndSync
{
FRenderCommandFence Fence[2]; // 렌더링(Render)배리어/펜스 .
int32 EventIndex; // 현재(Current)이벤트
public:
// 동기화 스레드(Thread) 및 렌더링(Render)스레드(Thread). bAllowOneFrameThreadLag는 렌더링(Render)스레드(Thread)의 레이턴시 .
void Sync( bool bAllowOneFrameThreadLag )
{
Fence[EventIndex].BeginFence(true); // 활성화(Enable)배리어/펜스 , 동기화 RHI 및 GPU스왑체인 의 .
bool bEmptyGameThreadTasks = !FTaskGraphInterface::Get().IsThreadProcessingTasks(ENamedThreads::GameThread);
// 스레드(Thread)회 .
if (bEmptyGameThreadTasks)
{
FTaskGraphInterface::Get().ProcessThreadUntilIdle(ENamedThreads::GameThread);
}
// 만약 레이턴시 , 이벤트 .
if( bAllowOneFrameThreadLag )
{
EventIndex = (EventIndex + 1) % 2;
}
(......)
// 활성화(Enable)배리어/펜스 .
Fence[EventIndex].Wait(bEmptyGameThreadTasks);
}
};FFrameEndSync의 사용은 FEngineLoop::Tick에서 이루어집니다.
// Engine\Source\Runtime\Launch\Private\LaunchEngineLoop.cpp
void FEngineLoop::Tick()
{
(......)
// 루프 의 추가(Add)스레드(Thread) 및 렌더링(Render)스레드(Thread)의 동기화 이벤트 .
{
static FFrameEndSync FrameEndSync; // 로컬 정적 변수, 스레드(Thread).
static auto CVarAllowOneFrameThreadLag = IConsoleManager::Get().FindTConsoleVariableDataInt(TEXT("r.OneFrameThreadLag"));
// 동기화 및 렌더링(Render)스레드(Thread), 는 의 레이턴시 에 의해 . 기본값: 활성화(Enable)의 .
FrameEndSync.Sync( CVarAllowOneFrameThreadLag->GetValueOnGameThread() != 0 );
}
(......)
}2.6 멀티스레드 렌더링 결론
병렬 컴퓨팅 아키텍처는 현대 엔진의 표준 구성이 되었으며, UE의 멀티 스레드 렌더링은 멀티 코어 CPU 및 차세대 그래픽 API의 탄생과 함께 필연적인 제품입니다. 그러나 현재 렌더링 스레드는 대부분의 경우 여전히 단일입니다(TaskGraph의 도움으로 부분적으로 병렬화될 수 있음에도 불구하고). 이상적으로는 여러 렌더링 스레드가 병렬 및 독립적으로 렌더링 명령을 생성하며 기본 스레드에 의해 구동될 필요가 없습니다. 모든 스레드는 작업자 스레드로 사용될 수 있으며(즉, UE의 명명된 스레드가 없음) 운영 체제 수준의 기능 스레드를 피하면서 모든 스레드가 컴퓨팅 작업을 시작할 수 있습니다. 이를 달성하려면 운영 체제, 그래픽 API 및 컴퓨터 언어의 지속적인 발전이 필요합니다.
최근 출시된 UE4.26은 이미 RDG를 대중화했습니다. RDG는 렌더링 패스와 리소스를 자동으로 자르고 최적화할 수 있으며, 이는 엔진의 전반적인 병렬 처리를 개선하는 데 유용한 도구입니다.
이 글은 당초 약 2개월 안에 완성될 것으로 예상됐으나 실제로는 3개월 이상이 소요돼 저자의 여가 시간을 거의 다 소모했다. 원래 추가해야 할 기술적인 챕터가 많았으나 공간과 시간의 한계가 초과되어 포기할 수 밖에 없었습니다. 이 시리즈의 기사가 UE를 배우고 있는 독자들에게 도움이 되기를 바랍니다. 많은 관심과 수집 부탁드립니다.
특별 지침
모든 참고문헌의 저자에게 감사드립니다. 일부 사진은 참고 자료와 인터넷에서 가져온 것이므로 삭제되었습니다.
이 시리즈의 기사는 저자가 직접 작성한 것이며 블로그에만 게시됩니다. 이 기사의 링크를 공유하는 것은 환영하지만 무단 복제는 허용되지 않습니다!
계속되는 일련의 기사. 전체 목록을 보려면 콘텐츠 개요를 클릭하세요.
계속되는 일련의 기사. 전체 목록을 보려면 콘텐츠 개요를 클릭하세요.
계속되는 일련의 기사. 전체 목록을 보려면 콘텐츠 개요를 클릭하세요.
📚 참고 자료 및 공식 링크 (References)
- Unreal Engine 4 Sources
- Unreal Engine 4 Documentation
- Rendering and Graphics
- Materials
- Graphics Programming
- Unreal Engine 4 Rendering
- Threaded Rendering
- 『-언리얼 엔진(Unreal Engine)개요 분석』
- 『-언리얼 엔진 4 (UE4)렌더링아키텍처분석 및 고찰』
- UE4 Render System Sheet
- Inside UE4
- Exploring in UE4
- 『게임 엔진 아키텍처 (Game Engine Architecture)』
- C++ reference
- 『C++멀티스레드프로그래밍』
- 『Win32멀티스레드』
- 『Windows프로그래밍(제5판)』
- 《Effective C++》
- 《C++ Concurrency In Action》
- 《Modern Concurrency in Depth》
- 《Multithreaded Programming Guide》
- Windows 95
- Amdahl's law
- Thread-local storage
- Covering Multithreading Basics
- Multi-Threaded Programming: C++11
- Coroutine()원리 및 메커니즘및Coroutine in Kotlin
- C++11 멀티스레드
- C++ 및 개발[6]:멀티스레드
- volatile (computer programming)
- C/C++ 내의 volatile
- Practical Parallel Rendering with DirectX 9 and 10
- Multi-engine synchronization
- Understanding DirectX Multithreaded Rendering Performance by Experiments
- Performance, Methods, and Practices of DirectX 11 Multithreaded Rendering
- Introduction to Multithreaded rendering and the usage of Deferred Contexts in DirectX 11
- Practical Parallel Rendering with DirectX 9 and 10
- D3D11 및 D3D12멀티스레드렌더링
- Render graphs and Vulkan — a deep dive
- DirectX 12: Performance Comparison Between Single- and Multithreaded Rendering when Culling Multiple Lights
- Vulkan - 렌더링
- Vulkan 멀티스레드렌더링
- Vulkan Multi-Threading
- API VULKAN AR、VR
- Fork-Join Model
- Fiber (computer science)
- Evaluation of multi-threading in Vulkan
- Metal: Command Organization and Execution Model
- Task-based Multithreading - How to Program for 100 cores
- Multi-Threaded Game Engine Design
- 0x04:아키텍처
- Multithreaded Rendering & Graphics Jobs 멀티스레드렌더링 및 Jobs
- Optimizing Graphics in Unity
- Frostbite Rendering Architecture and Real-time Procedural Shading & Texturing Techniques (GDC 2007)
- FrameGraph: Extensible Rendering Architecture in Frostbite
- Parallelizing the Naughty Dog engine using fibers
- Destiny’s Multi-threaded Renderer Architecture talk
- Multithreading the Entire Destiny Engine
- Everything You Need to Know About Multithreaded Rendering in Fortnite
- bgfx
- 언리얼 엔진 4 (UE4) Task Graph System 소개
- UE4TaskGraph
- Directed acyclic graph
- Scalability for All: Unreal Engine* 4 with Intel
- UE심층 분석기술
- UE4 관련: 문헌/자료
- Intel® ISPC User's Guide
- 멀티스레드렌더링
- UE4 렌더링
- UE4 및 렌더링
- Bringing Fortnite to Mobile with Vulkan and OpenGL ES