ݺߣ

ݺߣShare a Scribd company logo
비동기 파일 로딩
1
목차
• 시작하며…
• 비동기 파일 로딩 과정
• 특정 스레드로 특정 작업을 위임하기
• 최종 데이터 가공 완료 통보하기
• 마치며…
2
시작하며…
개인 프로젝트에서 구현해본 비동기 파일 로딩 방식에 대해서 이야기 하고자
합니다.
주 스레드가 멈추지 않고 백그라운드에서 에셋 파일을 읽고 가공하여 전달하는
일련의 과정을 다룹니다.
3
비동기 파일 로딩 과정
에셋을 불러들이는 과정을 살펴보겠습니다.
주로 2개의 스레드가 사용되며 데이터 가공 과정에서 분산 처리를 위해 4개의
스레드가 사용됩니다.
Main thread
Filesystem thread
Storage
Worker threads(4개)
4
비동기 파일 로딩 과정
IOCP를 사용한 비동기 파일 I/O를 사용해서 구현했기 때문에 프로그램이
실행되면 파일시스템 스레드는 자신을 대기 큐에 등록합니다.
Main thread
Filesystem thread
Storage
Worker threads(4개)
스레드 시작부터
대기 상태로 진입
5
비동기 파일 로딩 과정
메인 스레드에서 에셋을 로딩하기 위해서 비동기 파일 읽기 함수를 호출하고
2차 저장장치에서 파일을 읽어 메인 메모리에 적재하게 됩니다.
Main thread
Filesystem thread
요청
Storage
파일 읽는 중
Worker threads(4개)
6
비동기 파일 로딩 과정
읽기 작업이 완료되면 대기하고 있던 파일시스템 스레드는 완료를 통보 받아
깨어납니다.
Main thread
Filesystem thread
요청
Storage
파일 읽기
깨어남
Worker threads(4개)
7
비동기 파일 로딩 과정
깨어난 파일시스템 스레드는 데이터의 버퍼와 크기를 메인 스레드로 전달한
다음 다시 자신을 대기 큐에 등록합니다.
Main thread
Filesystem thread
요청
Storage
파일 읽기
통보 후 재 대기
Worker threads(4개)
8
비동기 파일 로딩 과정
메인 스레드는 전달받은 데이터의 가공을 다른 워커 스레드로 위임합니다.
Main thread
Filesystem thread
요청
Storage
파일 읽기
통보 후 재 대기
가공 요청
Worker threads(4개)
에셋 가공
분산 처리 전용 스레드에서
처리하도록 위임
9
비동기 파일 로딩 과정
워커 스레드는 데이터를 게임에서 사용할 형태로 가공하면서 추가로 읽어야 할
파일이 있다면 이를 메인 스레드에 요청합니다.
Main thread
Filesystem thread
요청
Storage
파일 읽기
통보 후 재 대기
가공 요청
Worker threads(4개)
에셋 가공
파일 I/O에 대한 요청은
항상 메인 스레드에서
수행하도록
10
비동기 파일 로딩 과정
추가 파일에 대한 파일 읽기 진행 단계는 첫 요청과 동일합니다
Main thread
Filesystem thread
요청
Storage
파일 읽기
통보 후 재 대기
가공 요청
Worker threads(4개)
에셋 가공
요청
파일 읽기
통보 후 재 대기
가공 요청
에셋 가공
11
비동기 파일 로딩 과정
모든 읽기 작업이 완료되면 최종적으로 가공된 에셋 데이터가 메인 스레드로
전달됩니다.
Main thread
Filesystem thread
요청
Storage
파일 읽기
통보 후 재 대기
가공 요청
Worker threads(4개)
에셋 가공
요청
파일 읽기
통보 후 재 대기
가공 요청
가공 후 최종 데이터 전달
이후 로직 수행
12
비동기 파일 로딩 과정
일련의 로딩 과정에서 크게 2가지 이슈를 처리해야 했습니다.
1. 특정 스레드로 특정 작업을 위임할 수 있는 구조가 필요.
2. 추가 파일 데이터에 대한 읽기 작업 요청으로 인한 최종 완료 통보 시점 지연.
13
특정 스레드로 특정 작업 위임하기
기본적인 구현 방법은 태스크 시스템 형태의 스레드 풀입니다.
여러 개의 스레드를 역할을 나눠 미리 생성하였습니다.
14
특정 스레드로 특정 작업 위임하기
생성하는 스레드의 역할은 열거형으로 다음과 같이 정의되어 있습니다.
주 스레드인 GameThread를 제외한 6개의 스레드가 생성됩니다.
15
특정 스레드로 특정 작업 위임하기
그리고 태스크에는 workerAffinity 라는 변수를 통해서 어떤 스레드에서 작업을
수행할지 지정합니다.
workerAffinity는 스레드의 열거형을 기반으로 하여 비트 시프트를 통해 계산
합니다.
16
특정 스레드로 특정 작업 위임하기
이렇게 계산된 workerAffinityMask 와 스레드의 타입을 검사하여 해당 태스크를
스레드에서 실행할지를 결정합니다.
17
특정 스레드로 특정 작업 위임하기
실제 태스크 제출에는 다음과 같은 헬퍼 함수가 사용됩니다.
실제 호출 예시입니다. ( 주 스레드에서 실행되도록 작업을 위임 )
18
최종 데이터 가공 완료 통보하기
에셋의 종류에 따라서 추가적인 파일 읽기가 필요한 경우가 있습니다.
정적 메시 에셋의 클래스인 StaticMesh 클래스의 구조를 살펴보겠습니다.
모델 정점 데이터
재질 데이터
그 외
텍셀 데이터
텍스처
Vertex Shader
Pixel Shader
StaticMesh class Material class
DDSTexture class
버택스 셰이더
바이트 코드
VertexShader class
픽셀 셰이더
바이트 코드
PixelShader class
19
최종 데이터 가공 완료 통보하기
이런 구조로 인해서 모델 에셋을 온전하게 사용할 수 있는 시점은 모든 추가
파일의 데이터 읽기와 가공이 완료된 시점입니다.
추가 파일에 대한 처리의 완료를 기다리지 않고 로딩 완료 콜백을 호출한다면
필요한 데이터가 누락되는 등의 문제가 발생할 수 있습니다.
최종 완료 콜백은 이러한 처리가 모두 끝난 마지막에 호출되도록 적절하게
미뤄져야 합니다.
20
최종 데이터 가공 완료 통보하기
다음과 같은 단순한 선형 구조라면 완료 콜백을 지연시키기 위해 콜백체인을
이용할 수 있겠습니다.
A B C
21
최종 데이터 가공 완료 통보하기
하지만 이런 선형 구조는 추가 파일을 여러 개 로드해야 하는 경우에는 처리하기
어려워 집니다.
다음과 같은 구조에서 B 파일을 처리하고 난 다음 A 파일의 콜백을 바로 부를 수
없습니다. ( C, D를 아직 안 읽었기 때문 )
A C
B
D 22
최종 데이터 가공 완료 통보하기
이를 해결 하기 위해 종속 관계를 저장해서 콜백 호출을 무시하도록 하였습니다.
주요 변수 2개를 살펴보겠습니다.
A C
B
D
23
최종 데이터 가공 완료 통보하기
m_prerequisites 는 현재 에셋보다 먼저 처리되야 하는 에셋의 숫자입니다.
A C
B
D
Prerequisites : 3
Prerequisites : 0
Prerequisites : 0
Prerequisites : 0
24
최종 데이터 가공 완료 통보하기
m_prerequisites 이 0보다 큰 경우 다음과 같이 콜백의 호출이 무시됩니다.
A C
B
D
Prerequisites : 3
Prerequisites : 0
Prerequisites : 0
Prerequisites : 0
25
최종 데이터 가공 완료 통보하기
m_subSequentList 는 현재 에셋의 처리를 기다리고 있는 에셋 로더 핸들의
리스트입니다.
A C
B
D
subSequenceList = [A]
subSequenceList = [A]
subSequenceList = [A]
subSequenceList = []
26
최종 데이터 가공 완료 통보하기
현재 에셋의 처리가 끝나면 m_subSequentList 에 등록된 핸들에 대해
DecreasePrerequisite( ) 함수를 호출해서 m_prerequisites 를 감소 시킵니다.
A C
B
D
Prerequisites : 2
Prerequisites : 0
Prerequisites : 0
Prerequisites : 0
B 에셋의 처리가 끝나면
B에셋의 처리를 기다리는
A 에셋의 Prerequisites 값을 1 감소
27
최종 데이터 가공 완료 통보하기
종속된 모든 에셋의 처리가 끝나게 되면 m_prerequisites의 값이 0이 되어 최종
적으로 A 에셋의 콜백을 호출할 수 있게 됩니다.
A C
B
D
Prerequisites : 0
Prerequisites : 0
Prerequisites : 0
Prerequisites : 0
28
최종 데이터 가공 완료 통보하기
이런 식으로 완료 콜백을 지연합니다. 이제 에셋 간 종속 관계를 파일 처리
과정에서 적절하게 연결하면 됩니다.
저는 현재 처리중인 에셋에 대한 에셋 로더 핸들을 다음과 같이 저장하고
파일 시스템에 비동기 읽기 요청 시 연결해주는 형태로 구현하였습니다.
29
읽기 완료 후 에셋 가공을
워커 스레드로 위임하는 코드
최종 데이터 가공 완료 통보하기
지금까지의 과정은 DAG(Directed acyclic graph) 를 파일 읽기 과정에서
만드는 것입니다.
m_subSequentList 가 진입 간선( Incoming edge )의 개수 라고 생각하시면
될 것입니다.
30
마치며…
제가 구현해본 비동기 파일 로딩 과정은 이것이 전부입니다.
현재 로딩 중인 파일에 대한 읽기 요청이 또 들어 온 경우의 처리 등 자잘한 예외
처리 사항이 남아 있긴 하지만 핵심내용은 아니라 제외하였습니다.
31
더 자세한 코드를 보고 싶으시다면...
ppt의 코드는 설명을 위해 전체 코드의 일부분을 발췌하였습니다.
전체 코드는 아래 링크를 참조 바랍니다.
• https://github.com/xtozero/SSR/blob/multi-
thread/Source/Engine/Public/AssetLoader/AssetLoader.h
• https://github.com/xtozero/SSR/blob/multi-
thread/Source/Engine/Private/AssetLoader/AssetLoader.cpp
• https://github.com/xtozero/SSR/blob/multi-
thread/Source/Core/Public/TaskScheduler.h
• https://github.com/xtozero/SSR/blob/multi-
thread/Source/Core/Private/TaskScheduler.cpp
32

More Related Content

비동기 파일 로딩

  • 2. 목차 • 시작하며… • 비동기 파일 로딩 과정 • 특정 스레드로 특정 작업을 위임하기 • 최종 데이터 가공 완료 통보하기 • 마치며… 2
  • 3. 시작하며… 개인 프로젝트에서 구현해본 비동기 파일 로딩 방식에 대해서 이야기 하고자 합니다. 주 스레드가 멈추지 않고 백그라운드에서 에셋 파일을 읽고 가공하여 전달하는 일련의 과정을 다룹니다. 3
  • 4. 비동기 파일 로딩 과정 에셋을 불러들이는 과정을 살펴보겠습니다. 주로 2개의 스레드가 사용되며 데이터 가공 과정에서 분산 처리를 위해 4개의 스레드가 사용됩니다. Main thread Filesystem thread Storage Worker threads(4개) 4
  • 5. 비동기 파일 로딩 과정 IOCP를 사용한 비동기 파일 I/O를 사용해서 구현했기 때문에 프로그램이 실행되면 파일시스템 스레드는 자신을 대기 큐에 등록합니다. Main thread Filesystem thread Storage Worker threads(4개) 스레드 시작부터 대기 상태로 진입 5
  • 6. 비동기 파일 로딩 과정 메인 스레드에서 에셋을 로딩하기 위해서 비동기 파일 읽기 함수를 호출하고 2차 저장장치에서 파일을 읽어 메인 메모리에 적재하게 됩니다. Main thread Filesystem thread 요청 Storage 파일 읽는 중 Worker threads(4개) 6
  • 7. 비동기 파일 로딩 과정 읽기 작업이 완료되면 대기하고 있던 파일시스템 스레드는 완료를 통보 받아 깨어납니다. Main thread Filesystem thread 요청 Storage 파일 읽기 깨어남 Worker threads(4개) 7
  • 8. 비동기 파일 로딩 과정 깨어난 파일시스템 스레드는 데이터의 버퍼와 크기를 메인 스레드로 전달한 다음 다시 자신을 대기 큐에 등록합니다. Main thread Filesystem thread 요청 Storage 파일 읽기 통보 후 재 대기 Worker threads(4개) 8
  • 9. 비동기 파일 로딩 과정 메인 스레드는 전달받은 데이터의 가공을 다른 워커 스레드로 위임합니다. Main thread Filesystem thread 요청 Storage 파일 읽기 통보 후 재 대기 가공 요청 Worker threads(4개) 에셋 가공 분산 처리 전용 스레드에서 처리하도록 위임 9
  • 10. 비동기 파일 로딩 과정 워커 스레드는 데이터를 게임에서 사용할 형태로 가공하면서 추가로 읽어야 할 파일이 있다면 이를 메인 스레드에 요청합니다. Main thread Filesystem thread 요청 Storage 파일 읽기 통보 후 재 대기 가공 요청 Worker threads(4개) 에셋 가공 파일 I/O에 대한 요청은 항상 메인 스레드에서 수행하도록 10
  • 11. 비동기 파일 로딩 과정 추가 파일에 대한 파일 읽기 진행 단계는 첫 요청과 동일합니다 Main thread Filesystem thread 요청 Storage 파일 읽기 통보 후 재 대기 가공 요청 Worker threads(4개) 에셋 가공 요청 파일 읽기 통보 후 재 대기 가공 요청 에셋 가공 11
  • 12. 비동기 파일 로딩 과정 모든 읽기 작업이 완료되면 최종적으로 가공된 에셋 데이터가 메인 스레드로 전달됩니다. Main thread Filesystem thread 요청 Storage 파일 읽기 통보 후 재 대기 가공 요청 Worker threads(4개) 에셋 가공 요청 파일 읽기 통보 후 재 대기 가공 요청 가공 후 최종 데이터 전달 이후 로직 수행 12
  • 13. 비동기 파일 로딩 과정 일련의 로딩 과정에서 크게 2가지 이슈를 처리해야 했습니다. 1. 특정 스레드로 특정 작업을 위임할 수 있는 구조가 필요. 2. 추가 파일 데이터에 대한 읽기 작업 요청으로 인한 최종 완료 통보 시점 지연. 13
  • 14. 특정 스레드로 특정 작업 위임하기 기본적인 구현 방법은 태스크 시스템 형태의 스레드 풀입니다. 여러 개의 스레드를 역할을 나눠 미리 생성하였습니다. 14
  • 15. 특정 스레드로 특정 작업 위임하기 생성하는 스레드의 역할은 열거형으로 다음과 같이 정의되어 있습니다. 주 스레드인 GameThread를 제외한 6개의 스레드가 생성됩니다. 15
  • 16. 특정 스레드로 특정 작업 위임하기 그리고 태스크에는 workerAffinity 라는 변수를 통해서 어떤 스레드에서 작업을 수행할지 지정합니다. workerAffinity는 스레드의 열거형을 기반으로 하여 비트 시프트를 통해 계산 합니다. 16
  • 17. 특정 스레드로 특정 작업 위임하기 이렇게 계산된 workerAffinityMask 와 스레드의 타입을 검사하여 해당 태스크를 스레드에서 실행할지를 결정합니다. 17
  • 18. 특정 스레드로 특정 작업 위임하기 실제 태스크 제출에는 다음과 같은 헬퍼 함수가 사용됩니다. 실제 호출 예시입니다. ( 주 스레드에서 실행되도록 작업을 위임 ) 18
  • 19. 최종 데이터 가공 완료 통보하기 에셋의 종류에 따라서 추가적인 파일 읽기가 필요한 경우가 있습니다. 정적 메시 에셋의 클래스인 StaticMesh 클래스의 구조를 살펴보겠습니다. 모델 정점 데이터 재질 데이터 그 외 텍셀 데이터 텍스처 Vertex Shader Pixel Shader StaticMesh class Material class DDSTexture class 버택스 셰이더 바이트 코드 VertexShader class 픽셀 셰이더 바이트 코드 PixelShader class 19
  • 20. 최종 데이터 가공 완료 통보하기 이런 구조로 인해서 모델 에셋을 온전하게 사용할 수 있는 시점은 모든 추가 파일의 데이터 읽기와 가공이 완료된 시점입니다. 추가 파일에 대한 처리의 완료를 기다리지 않고 로딩 완료 콜백을 호출한다면 필요한 데이터가 누락되는 등의 문제가 발생할 수 있습니다. 최종 완료 콜백은 이러한 처리가 모두 끝난 마지막에 호출되도록 적절하게 미뤄져야 합니다. 20
  • 21. 최종 데이터 가공 완료 통보하기 다음과 같은 단순한 선형 구조라면 완료 콜백을 지연시키기 위해 콜백체인을 이용할 수 있겠습니다. A B C 21
  • 22. 최종 데이터 가공 완료 통보하기 하지만 이런 선형 구조는 추가 파일을 여러 개 로드해야 하는 경우에는 처리하기 어려워 집니다. 다음과 같은 구조에서 B 파일을 처리하고 난 다음 A 파일의 콜백을 바로 부를 수 없습니다. ( C, D를 아직 안 읽었기 때문 ) A C B D 22
  • 23. 최종 데이터 가공 완료 통보하기 이를 해결 하기 위해 종속 관계를 저장해서 콜백 호출을 무시하도록 하였습니다. 주요 변수 2개를 살펴보겠습니다. A C B D 23
  • 24. 최종 데이터 가공 완료 통보하기 m_prerequisites 는 현재 에셋보다 먼저 처리되야 하는 에셋의 숫자입니다. A C B D Prerequisites : 3 Prerequisites : 0 Prerequisites : 0 Prerequisites : 0 24
  • 25. 최종 데이터 가공 완료 통보하기 m_prerequisites 이 0보다 큰 경우 다음과 같이 콜백의 호출이 무시됩니다. A C B D Prerequisites : 3 Prerequisites : 0 Prerequisites : 0 Prerequisites : 0 25
  • 26. 최종 데이터 가공 완료 통보하기 m_subSequentList 는 현재 에셋의 처리를 기다리고 있는 에셋 로더 핸들의 리스트입니다. A C B D subSequenceList = [A] subSequenceList = [A] subSequenceList = [A] subSequenceList = [] 26
  • 27. 최종 데이터 가공 완료 통보하기 현재 에셋의 처리가 끝나면 m_subSequentList 에 등록된 핸들에 대해 DecreasePrerequisite( ) 함수를 호출해서 m_prerequisites 를 감소 시킵니다. A C B D Prerequisites : 2 Prerequisites : 0 Prerequisites : 0 Prerequisites : 0 B 에셋의 처리가 끝나면 B에셋의 처리를 기다리는 A 에셋의 Prerequisites 값을 1 감소 27
  • 28. 최종 데이터 가공 완료 통보하기 종속된 모든 에셋의 처리가 끝나게 되면 m_prerequisites의 값이 0이 되어 최종 적으로 A 에셋의 콜백을 호출할 수 있게 됩니다. A C B D Prerequisites : 0 Prerequisites : 0 Prerequisites : 0 Prerequisites : 0 28
  • 29. 최종 데이터 가공 완료 통보하기 이런 식으로 완료 콜백을 지연합니다. 이제 에셋 간 종속 관계를 파일 처리 과정에서 적절하게 연결하면 됩니다. 저는 현재 처리중인 에셋에 대한 에셋 로더 핸들을 다음과 같이 저장하고 파일 시스템에 비동기 읽기 요청 시 연결해주는 형태로 구현하였습니다. 29 읽기 완료 후 에셋 가공을 워커 스레드로 위임하는 코드
  • 30. 최종 데이터 가공 완료 통보하기 지금까지의 과정은 DAG(Directed acyclic graph) 를 파일 읽기 과정에서 만드는 것입니다. m_subSequentList 가 진입 간선( Incoming edge )의 개수 라고 생각하시면 될 것입니다. 30
  • 31. 마치며… 제가 구현해본 비동기 파일 로딩 과정은 이것이 전부입니다. 현재 로딩 중인 파일에 대한 읽기 요청이 또 들어 온 경우의 처리 등 자잘한 예외 처리 사항이 남아 있긴 하지만 핵심내용은 아니라 제외하였습니다. 31
  • 32. 더 자세한 코드를 보고 싶으시다면... ppt의 코드는 설명을 위해 전체 코드의 일부분을 발췌하였습니다. 전체 코드는 아래 링크를 참조 바랍니다. • https://github.com/xtozero/SSR/blob/multi- thread/Source/Engine/Public/AssetLoader/AssetLoader.h • https://github.com/xtozero/SSR/blob/multi- thread/Source/Engine/Private/AssetLoader/AssetLoader.cpp • https://github.com/xtozero/SSR/blob/multi- thread/Source/Core/Public/TaskScheduler.h • https://github.com/xtozero/SSR/blob/multi- thread/Source/Core/Private/TaskScheduler.cpp 32