Category Archives: Programming

IDispose와 Document에서 제안하는 사용법

Link : https://docs.microsoft.com/ko-kr/dotnet/standard/garbage-collection/implementing-dispose

C#에서 class의 destruct 방법을 찾아보다가 찾게 되었다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
using System;
class BaseClass : IDisposable
{
   // Flag: Has Dispose already been called?
   bool disposed = false;
   // Public implementation of Dispose pattern callable by consumers.
   public void Dispose()
   {
      Dispose(true);
      GC.SuppressFinalize(this);
   }
   // Protected implementation of Dispose pattern.
   protected virtual void Dispose(bool disposing)
   {
      if (disposed)
         return;
      if (disposing) {
         // Free any other managed objects here.
         //
      }
      // Free any unmanaged objects here.
      //
      disposed = true;
   }
   ~BaseClass()
   {
      Dispose(false);
   }
}
cs

 

소멸자가 불리는 경우 managed object의 경우에는 GC에서 콜렉팅 되도록 내버려 두는데, 이는 아마도 소멸자가 불리는 경우가 GC에 의해 콜렉팅 된 경우라고 가정하고 해당 클래스 내부의 managed object들도 이미 GC에 잡혀 있기 때문에 소멸자가 불린 시점에서 정리를 해주게 되면 GC가 이미 정리된 부분들에 접근하게 되므로 문제가 생기거나 이중처리로 인한 지연을 고려하여 이렇게 구현한 것이라 생각된다.

Simple Multithreading for Unity 초벌 번역

이 아티클은 원작자 Richard Meredith의 허락을 얻은 후 원문인 Simple Multithreading for Unity를 한글로 번역되었습니다. 아직 초벌 번역이므로 오히려 불편하신 분들은 원문을 참고하시면 됩니다.

Introduction

나는 최근에 Bad North의 flow field(흐름을 나타내는 필드)의 알고리즘의 비용을 줄이기 위해 유니티에서의 multithreading의 기초에 대해 살펴보았다.
모바일 환경에서 메인스레드가 처리하는데 걸리는 시간을 2.75ms 에서 0.25ms까지 낮추게 되면서 나는 아주 기초적인 유니티에서의 multithreading에 대해 글을 남기는 게 좋겠다고 생각했다.
나는 이 글이 비록 이전에 threaded code에 대한 경험이 없더라도 threading가 무엇인지에 대해 그리고 유니티/C# 스크립트에 대한 조금의 이라도 도움이 될수 있도록 누구나 접근가능했으면 좋겠다.

Important Disclaimers

1) 나는 알고리즘 자체를 별렬화 하지는 않았다. 내가 한 것은 게임의 남는 시간동안 다른 thread에서 이것이 돌도록 밀어넣은 것 뿐이다.
2) 이것은 C#/.NET환경에서 어떤것을 threading 나의 첫번째이다.(C++에서는 몇번 경험해 봤지만…) 그래서 더 개선의 여지가 있을 수 있다.

The Flow Field Algorithm

나는 알고리즘의 상세한 부분에 대해서 이야기 하지는 않을 것이다. 하지만 대략적으로 flow field는 다음에 언급되어 있는 네비메쉬에 걸쳐 펼쳐져 있는 데이터이다.

– 근처에 얼마나 많은 agent들이 있는지
– 그들이 얼마나 가까이 있는가
– 그들이 어느 방향에 있는가

매 프레임마다. 모든 에이전트들은 “액체”를 flow field에 떨어트린다. 이 것을 가까운 버텍스들에 축적한다. 이 액체는 네비메쉬 vertex들 주변에 따라 흐르고 또 증발한다. 이런 효과들은 아래의 gif에서 파란 혹은 주황 색의 디버그 그래픽들로 시각화되어있다.

Identifying Actions

다음은 flow field 정보들에게 일어나는 액션들이다.

1)필드기반의 시스템 샘플 데이터
2)에이전트들이 filed의 데이터에 영향을 준다.
3)전파(전달 혹은 flow 업데이트)가 일어난다.

이 과정은 main thread에서 다음과 순서로 일어난다.

이 포스트에 있는 모든 thread diagrams(스레드 도식)은 단순화 되어 있고 수평축(시간축)에 축적되지 않는다.

다양한 시스템/스크립트들이 여러 프레임에 걸쳐 flow field에 쓰여지고 읽혀진다.(초록색 화살표처럼) flow 업데이트 동안 많은 읽기와 쓰기가 flow field에 행해진다. flow update는 꽤 많은 비용이 들어가는 부분이고 이 부분이 우리가 다른 스레드로 분리하고 싶은 부분이다. 이걸 분리하면 생길 몇가지 있겠지만 이부분은 조금 더 뒤에 걱정하도록 하자.

자 우선 thread를 하나 만들자.

Creating The Thread

나는 threaded system을 약간의 반복 시행하여 살펴보았고 작동하지 않는 명백한 해결법을 동작하지 않는 이유로 인해 폐기했다. 나의 첫 시도는 매 프레임마다 새로운 thread를 생성하여 작업을 수행하고 사라지도록 하는 것이었습니다.
하지만 thread를 생성하는 것은 매우 느리고(flow field를 업데이트 하는 것과 거의 비슷할 정도로) 매 프레임마다 500바이트의 Garbage를 생성했다.

내 두번째 시도는 셋업 시간을 피하기 위해 thread들을 풀링하는 BackgroundWorker시스템을 이용하는 것이다. 빠르긴 하지만 여전히 500바이트의 Garbage를 매 프레임 생성한다.

Chosen Solution

나의 해결책은 조금 더 로우 레벨로 가는 것이었다. 나는 간단하게 동작하는 무한루프인 하나의 thread를 생성하고 main Thread의 Unity Update()와 동기화 시켰다. 이는 아래와 같다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
using System.Threading;
using UnityEngine;
public class ThreadedBehaviour : MonoBehaviour
{
    Thread ChildThread = null;
    EventWaitHandle ChildThreadWait = new EventWaitHandle(true, EventResetMode.ManualReset);
    EventWaitHandle MainThreadWait = new EventWaitHandle(true, EventResetMode.ManualReset);
    void ChildThreadLoop()
    {
        ChildThreadWait.Reset();
        ChildThreadWait.WaitOne();
        while(true)
        {
            ChildThreadWait.Reset();
            // Do Update
            WaitHandle.SignalAndWait(MainThreadWait, ChildThreadWait);
        }
    }
    void Awake()
    {
        ChildThread = new Thread(ChildThreadLoop);
        ChildThread.Start();
        }
    void Update()
    {
        MainThreadWait.Reset();
        WaitHandle.SignalAndWait(ChildThreadWait, MainThreadWait);
    }
}
cs

Script Overview

여기서 중요한것은 thread들을 동기화 하기 위한 두개의 EventWaitHandle 변수이다. 만일 thread가 reset된 waithandle에 의해 기다리기를 요청 받는다면 다른 thread가 Set을 콜할 때까지 block(기다릴)될 것이다.(13번 라인 참조)
SignalAndWait() 함수는 두 파라미터의 Set()과 WaitOne()를 차례로 콜한 것과 같다.(효과적으로 다른 thread의 block을 해제하고 현재의 thread를 block할)

ThreadedBehaviour 클래스의 Awake()에서 child thread를 생성하고 ChildThreadLoop()내에서 코드를 시작할 것이고 즉시 ChildThreadWait를 기다릴 것이다. 이것은 Update() 함수가 불릴 때까지 block 상태로 남아있을 것이다.

Script Behaviour

Update()에서 우리는 child thread의 block을 해제하고 child block이 작업을 완료할 때까지 main thread 자신을 block할 것이다. 이 후 child thread가 자신의 작업을 완료하면 main thread는 block이 해제될 것이고 child thread가 block될 것이다.(21줄) 아래에 보이는 것 처럼…

이제 우리는 flow update를 main thread가 아닌 다른 thread에서 하고 있다. 하지만 여전히 다음작업은 flow update가 완료되기를 기다리고 있다. 우리는 main thread에서 어떠한 시간도 절약하지 못하고 있다. 실제로는 동기화를 하기 위한 약간의 오버헤드로 인해 더욱 느려졌다.
하지만 우리는 작업을 다른 thread로 넘기고 있고 thread들을 동기화 하는 방법을 알게 되었다. 우리는 병렬화하기위한 커다란 한걸음을 내딛은 것이다.

Parallelisation

이제 Flow Update는 다른 분리된 thread에서 동작하지만 main thread를 block하고 있기때문에 이건 제대로 된 multthreading도 아니고 시간도 절약하지 못하고 있다.

thread들을 병렬로 동작하도록 하기 위해서는 Update()함수를 다음과 같이 바꾸어 main thread의 block을 풀어주어야 한다.

31
32
33
34
    void Update()
    {
        ChildThreadWait.Set();
    }
cs

매우간단하게 우리는 thread들이 다음과 같이 동작하도록 만들었다.

최소한 우리가 바라는대로 돌아가도록 만든 것처럼 보이지만 실제로는 그렇지 않다. 이게 아마도 multithreading에서 가장 중용한 부분이지 않을까하는데 이 문제를 확인하고 풀기위해 우리는 한가지 사고 실험을 할 것이다.

Problems with Parallelisation

우리는 flow update가 얼마나 걸릴지 알 수가 없다. 특히 당신은 알수 없다 다른 스레드와의 관계가 얼마나 걸릴지. 당신이 이걸 테스트 해보고 제 시간에 끝나는 걸 확인했다면 당신은 이것이 아마 제 시간안에 끝날거라고 생각할 것이다.
하지만 multithread는 예측 불가능하고 테스트로는 안정적이라고 증명할 수 없다. 안정적인 디자인을 필요로 한다.

예를 들어 flow update가 기대한것 보다 오래걸린다고 가정해 보자.

두번째 프레임에 우린 두 thread등이 동시에 flow field에서 데이터를 읽고 쓰는 걸 볼 수 있다. 이건 정의되지 않았고 문제가 될만한 행위이다. 우린 또한 main thread가 child thread가 작업을 마치기도 전에 다시 시작하려고 시도하는 걸 볼 수 있는데 이건 반복될 것이고, 이건 괜찮지 않다.

Re-Synchronising The Threads

그래서 main thread를 update함수가 시작할 때 block하여 child thread가 main thread보다 더 시간이 걸리지 않도록 보장하는 것을 시작하자.

31
32
33
34
35
36
37
    void Update()
    {
        MainThreadWait.WaitOne();
        MainThreadWait.Reset();
        ChildThreadWait.Set();
    }
cs

첫 업데이트에서 우린 MainThreadWait 변수를 Set상태로 두어 대기 하지 않음을 알아두자. 반복되는 프레임에서 만일 child thread가 여전히 동작중이라면 main thread는 다음과 같이 기다릴 것이다.

이제 우리 두 loop들은 동기화 되어있지만, 여전히 두 thread들은 같은 data를 이용하며 병렬로 돌아가고 있다. 이걸 해결할 방법이 몇가지 있지만, 내가 선택한 방법은 thread들 간에 어떠한 것도 직접적으로 공유되지 않도록 데이터 구조를 조정하는 방법을 가지고 있다.

Restructuring Data

본래의 구현은 작업하는 하나의 데이터 세트만 가지고 있었다. 요약해서 이 데이터는 우리가 다음의 세가지 상호작용을 하던 데이터이다.
1)field로 부터의 System 샘플 데이터
2)Agent들이 필드에 추가한 데이터
3)전파 발생(Data의 전파, 즉 flow update)

우리는 이제 데이터를 다음 동작을 하는 것에 따라 3개의 세트로 나눌 것이다.
1)알고리즘의 결과(샘플링에 사용될)
2)미완료된 변화들(추가에 사용될)
3)사용되는 데이터(전파되는데 사용될 다양한 데이터)

Results

이것은 당신의 데이터 상태의 스냅샷이다. 즉 flow field이다. main thread와 child thread가 각각 자신이 사용할 정보를 가지고 있다. child thread는 자신의 data를 업데이트 할 것이고 그 정보를 매 프레임 main thread에게 복사 해 줄것이다.

중요 : 결과 값과 계산되지 않은 중간 값들은 value type이나 deep copy 이용하라. 데이터가 변하지 않는다는 것을 장담할 수 있더라도 referece를 복사하는 것은 절대 안된다.

Working Data

이 부분은 미적용된 데이터와 데이터를 가공하기 위한 여러 다른 단계를 거친 연산의 결과일 것이다.

Update Order

데이터구조는 어떤 프로그램을 짜느냐에 따라 달라지기 때문에 데이터 구조를 특정화하여 보여주진 않을 것이다. 그러나 main thread의 명령 순서는 다음과 같다:

31
32
33
34
35
36
37
38
39
40
void Update()
{
    MainThreadWait.WaitOne();
    MainThreadWait.Reset();
    // Copy Results out of the thread
    // Copy pending changes into the thread
    ChildThreadWait.Set();
}
cs

그리고 여기 더 복잡한 thread diagram이 있다.

이 그래프의 시간축은 단순한 시간의 흐름이라는 것을 명심(얼마나 긴지 길이의 비율은 각 명령에 걸리는 시간과 비례하지 않는다는 말)하고 실제 복사 명령은 굉장히 빠르게 일어난다. 값들의 동기 명령을 조작하는데 main thread가 담당하도록하는 것이 어떠한 충돌의 위험도 없다는 것을 쉽게 할수 있기 때문에 child thread의 데이터에서 copy를 해오고 해주는 작업에는 main thread를 사용한다.

일반적으로 우린 child thread가 flow update를 하는데 main thread보다 많은 시간이 걸리지 않을 거라고 생각하는데 우리는 단지 사고 실험을 통해 이것이 일어날 수도 있다고 가정을 하였다. 하지만 만일 실제로 일어나더라도 우린 더이상 두 thread들 사이에 어떠한 충돌도 일어나지 않는다. child thread는 이제 자신의 데이터만 읽고 쓰고 두 thread들은 적절하게 동기화 되어있다. 이제 multithread로 동작하게 되었고 data들도 thread safe(다른 스레드가 값을 사용하는도중에 또 다른 스레드가 값을 변경하거나 해서 충돌이 일어날 염려가 없다라는 것) 하다.

보통과 같이 flow update가 빠르게 끝나더라도 각 프레임에 thread들은 이렇게 동작할 것이다.

Summary/Thoughts

이건 내가 main thread에서 떼어내어 flow field update를 수행하기 위해 사용한 테크닉이다. 무조건 이 방법으로만 다른 thread로 data를 넘겨줄 수 있는건 아니지만 염두에 두어야 할 것이 몇가지 있다.

Main Thread Cost

다음에 대한 main thread가 지어야 할 몇몇 thread cost들이 있다.
0 미적용된 변화 데이터를 넘겨주는것
0 변화 값을 카피해 오는 것
0 결과 값을 카피해 주는 것
0 thread 동기화

대부분 스레드 동기화 입니다. EventWaitHandle에 대한 나의 이해가 단순한 thread 신호 주고 받기라는 것이기 때문에 나는 꽤나 놀랐다. 아마도 더 나은 방법도 있을거다.

Pipeline Delay

이 테크닉이 1프레임 지연하여 실행하는 것을 이해하는 것도 중요하다. 변화가 x라는 프레임에 대기열에 들어간다면 결과는 x+2까지 넘어가지는 않고 x+1프레임에 나온다. (설계상 1프레임 지연까지 결과가 나오지 않으면 대기하도록 되어있다.)
flow field의 경우에는 몇프레임에 걸쳐 전파되는 것이기 때문에 한프레임정도 지연 계산되더라도 상관없었다. 이건 왠만한 경우에 다 그럴 거라 여겨진다.

Garbage Collection

한가지 주의할 점은 카피를 할 때 Garbage를 생성하지 않도록 해야 한다는 것이다. 미적용된 변화 데이터들은 List 였고 이걸 복사하는 것은 단순히 list referece들을 복사하고 main thread를 위한 새로운 List를 만들면 되는 것이었다. 나는 리스트들의 각각의 요소들을 복사하는 것과 재사용가능한 버퍼로 그 것들을 다루어 Garbage가 생성되는 것을 피했다.

Exiting the Thread

여기에 포스팅한 예시에서를 thread를 죽이는 경우가 없다. 게임 내에서 thread의 종료에 대한 컨트롤을 하기 위해서 꽤나 투박한 방법을 사용하는데 (while loop에서 bool 변수를 이용해서 컨트롤하고 있다.), 유니티의 프레임워크를 이용해서 더 깔끔한 thread 종료 방법을 찾는 게 좋을 것 같다.

Thread Priority

난 flow update를 Late Update의 거의 마지막 부분에서 시작한다. 이렇게 하는게 프로젝트의 다른 스크립트와 관련된 부분들을 굉장히 예측하기 쉽기 때문이다. 하지만 이건 렌더링과 물리 업데이트와 겹치게 된다는 걸 의미하는데 두 부분에 대해서는 유니티에서 이미 multithreading을 하고 있다.(대부분의 타겟 플랫폼에서 그렇게 동작한다.) 최소한 유니티 5.5버전에서는 main thread와 child thread는 우선 순위가 제일 낮게 되어 있는데 아마 더 개선될 여지는 없어 보인다.(Thread 우선순위에 대한 개선 사항에 대한 이야기. 우선 순위가 높을 경우 더욱 개선할 방향이 있나보다)

Conclusion

copy에 대한 부하와 프레임 지연 연산에도 불구하고, 난 이 방법이 좋다. 엄청 간단하고 깔끔하다. background 작업은 모든 프레임에 업데이트 하고 thread들 간의 상호작용은 제한되어있다. 게다가 multithread화된 환경에서 copy와 sync를 하기 위한 부분을 제외하면 별다른 신경쓰지 않고 main thread는 데이터의 복사본과 상호작용할 수 있다.

한 프레임 지연과 데이터 복사가 필요없는 대안을 가진 접근방법도 있지만 꽤나 많은 단점을 가진 방법이다. thread를 종료하는 방법에 대한 몇몇 질문에 대한 답변은 별도의 포스트로 작정하겠다.

원글에 대한 저작권은 Richard Meredith에게 있습니다.

Copyright © 2017 Richard Meredith.

[프로그래밍] 주석과 가독성 by Kim Pope

주석에 대한 기존에 내 생각과 다르고 주석에 대한 고민을 해결해 줄 만한 이야기이다. 거의 혼자 코딩을 하다보니 가독성이 떨어지고 시간이 지나고 내가 봐도 이해하기 힘들만한 코드가 난무하고 있다. 거진 프로토타입에나 쓸법한 코드들이 난무하고 있는데… 우선은 잘 정리 할 수 있는 프로그래밍 패턴들에 대해서 잘 몰랐다.

주로 내가 작성한 코드들은 다음과 같은 현상들을 보였다.

1.코드의 재사용성이 매우 떨어졌다.

즉 비슷한 동작의 코드를 작성 할 때도 복사 붙여넣기 식의 코드를 많이 사용하게 되었다.

2.코드를 수정하다 보면 원치 않는 버그가 잔뜩 발생하는 경우가 많았다.

코드의 가독성이 떨어지게 되어 수정이 힘들어지고 한가지 동작을 하는 코드에 관여하는 변수나 잡다한 것들이 많아지면서 코드를 추가하거나 제거하면서 수정할 부분이 많아지고 그에 따른 실수들이 많아진다.

3.프로그래밍 패턴을 모르더라도 코드를 정리하는 습관이 점점 약해져갔다.

프로그래밍 패턴을 몰라도 나름 쓰기 쉽게 코드를 정리하고자 하는 편이었는데 지속적으로 혼자 작성하는 코드들을 만져가다 보니 코드 리팩토링등이나 코드 작성에 대한 고민을 적게하게 되어 갔다.

 

가장 큰 문제는 3번에 있지 않았을까 한다. 내 코드의 가독성이 떨어짐을 알면서도 개선을 하려는 의지가 없었던것. (사실 의지는 있었지만 귀찮았던… 뭐 행해지지 않은 일은 안한거랑 다를게 없지.)

 

위에 문제에 대해 김포프님의 생각은 오히려 주석을 이용하여 코드를 설명하는 것보다는 누가 읽어도(어느 정도 프로그래밍 지식이 있는 사람이야기 겠지) 간단하게 이게 어떤 동작을 위한 코드인지 쉽게 읽을 수 있도록 가독성 높은 코드를 짜는 것이 옳다 라는 입장인듯 하다.(인듯하다라고 쓴 것은 내가 동영상을 보고 그렇게 이해했다는 부분이다. 김포프님의 의도는 다를 수 있다.) 주석을 작성해야 하는 코드는 다른 사람들이 많이 사용하게 될 API에는 사용해야 한다라고도 이야기 한다. (API는 내부 소스가 공개되지 않는 경우가 많기 때문에 그런듯 하다.)

 

즉 코드 가독성 개선을 위해 지금 조금씩 공부하고 있는 프로그래밍 패턴의 중요성이 한층 더 올라갔다는 것!

[Unity] Event와 Delegate

http://www.unitygeek.com/delegates-events-unity/

얼마전 포스팅 했던 김포프님의 유니티 게임오브젝트 비디오에서 이야기 했던 부분 중 하나이다. 유니티는 C#을 이용하여 (그외에 JS를 모방한 자체 스크립트도 있지만…) 코드의 성능보다는 코드의 가독성과 독립성(? 코드간의 상호 의존성을 낮추는 것을 이야기 하는 것이다. 직접적인 접근을 피해 상호의존성을 낮춰서 혹시 수정했을 때 다른 곳에서 버그가 터지는 어리둥절한 일을 막기 위해서… 뒤로 넘어져서 코가 깨지는 거랑 같은 상황?)을 보장한다고 생각한다.(내 개인적으로… 애초에 프로그래밍적 지식이 없거나 적은 사람이 쉽게 게임을 제작하거나 할 수 있도록 하기 위해 만들어진 엔진이라고 한다.)

이벤트와 델리게이터는 직접 사용해 보지는 않았지만, 코코스를 사용하면서 어림잡아 알고 있는 대략적인 작동방식은(틀릴 수도 있다. 그냥 추측일 뿐…) 델리게이터는 특정 이벤트에 대한 리스너라고 볼 수 있다. 업데이트 문에 직접적으로 발생할 이벤트에 대한 트리거를 넣고 코드를 때려 넣어서 이벤트가 10개면 각종 트리거와 동작소스들로 엉키게 하는 것을 막고자 하는 것이다. 결국 동작 부분에서는 다를게 없겠지만 코드가 훨씬 깔끔해 질 수 있다는 거지. 특정 이벤트가 특정 델리게이터를 동작하게 할 것이다. 그럼 이벤트 종류별로 델리게이터를 분리 할 수 있을 것이고 혹시나 추가할 이벤트가 있다면 그 타입의 이벤트를 다루는 델리게이터쪽에서 추가하면 될 것이다.

아직 규모가 있는 프로그래밍을 해보지 않은 사람들은 그게 왜 필요하지? 그냥 하나에 다 박아 넣고 하나의 스크립트로 관리하면 되지라고 생각할 수 있겠지만(혹은 기억력이 좋아서 자신이 친 수천줄이 넘어가는 코드의 위치와 동작방식을 다 기억한다면…) 각종 이벤트가 100여개가 넘어가게 된다면 어찌될까? 그리고 1년 뒤 거기에서 쓰이지 않는 이벤트 몇개를 제거하고 다시 새롭게 더해진 이벤트 몇개를 추가하는 작업을 해야한다면?