Programing/Unity

Multi Threading in unity

Ezzi 2020. 2. 16. 01:14
반응형

 

엄밀히 말하면 이 글은 Unity의 스킬 이라기 보다는 멀티쓰레딩을 사용하는 방법론에 대한 이야기 입니다. 

 

제가 멀티 쓰레딩 관련 코드를 작성 하려고 한 시점이 회사에서 유니티 프로그램을 만들고 있던 중이여서

 

이 쪽 부분에 포스팅을 작성하면 아무래도 C# 에 익숙하지 않은 유니티 개발자들에게 도움이 될까 싶어서 주제를 이렇게 정하게 되었습니다. 

 

 

 

그럼 시작해 보겠습니다. 

 

 

인스턴스를 하나 생성하고 그 안에 멤버로 Queue가 하나 있다고 가정해 봅시다. 

 

그 Queue에 n개 만큼 아이템을 입력해 두고.

 

n개의 쓰레드에서 접근해서 하나씩 dequeue하는 것입니다. 

 

 

 

이 방법론으로 코드를 작성하기 전에 반드시 생각해야 하는 점들이 있습니다. 

 

2개의 쓰레드에서 하나의 인스턴스로 접근을 하게 될 것입니다. 

 

Critical Section이 발생하지 않도록 방지책을 마련해야 합니다. 

 

Thread를 사용하는 과정에서 Frame Drop이 발생할 수도 있다는 것을 인지하고 있어야 합니다. 

 

 

 

그럼 어떻게 구현하는지 하나씩 살펴 볼까요.

 

먼저 유니티쪽에서 코드를 작성 하기 전에 Queue를 Generic 클래스로 만들어 주는 작업을 먼저 할 것입니다.

 

이렇게 만들어 주게 되면 Queue를 여러 인스턴스에서 접근하려고 할 때 매번 Lock을 걸어줘야 할 필요가 없습니다. 

 

그리고 추후에 Object pool클래스를 확장할 수도 있는 장점도 가지고 있고요.

 

visual studio도 좋고 text 편집기도 좋으니 아무거나 열어서 다음 코드를 작성해 봅시다.

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
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
// helloezzi 2019.12.03
 
using System.Collections.Generic;
 
public interface IPoolable
{
    void Dispose();
}
 
public class ObjectPool<T> : IPoolable
{
    object lockObj = new object();
    Queue<T> queue = new Queue<T>();
 
    public void Push(T item)
    {
        lock (lockObj)
        {
            queue.Enqueue(item);
        }
    }
 
    public T Pop()
    {
        lock (lockObj)
        {
            return queue.Dequeue();
        }
    }
 
    public int Count
    {
        get
        {
            lock (lockObj)
            {
                return queue.Count;
            }
        }
    }
 
    public T Get()
    {
        lock (lockObj)
        {
            return queue.Peek();
        }
    }
 
    public void Clear()
    {
        lock (lockObj)
        {
            queue.Clear();
        }
    }
 
    public void Dispose()
    {
    }
}
cs

 

어떻게 구성되어 있는지 잠시 살펴 보겠습니다. 

 

가장 기본적으로 Queue가 멤버 변수로 있어야 하고 어떤 Type으로 생성할지는 Generic T 형태가 되겠죠.

 

Critical Section을 방지 하기 위해서 lock 변수도 생성해 줍니다. 

 

Method들을 살펴 볼까요.

 

Push()는 아이템을 하나 enqueue 합니다. 

 

Pop()은 아이템을 하나 dequeue 합니다. (Queue가 First In First Out인 것은 기초 상식 이겠죠.)

 

Count() 현재 Count를 리턴 합니다. 

 

Get()은 Peek 하는 함수 입니다.

Clear() 는 모두 지웁니다.

 

 

 

이제 본격적으로 유니티에서 코드를 작성해 보겠습니다. 

 

1. 그림과 같이 Scene에 Button 하나를 만들어 주고 Start라고 이름을 지어 줍니다. 

 

 

2. ThreadManager라는 이름으로 Script를 하나 만들어 주고 코드를 작성해 보겠습니다. 

 

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
38
39
40
41
42
43
44
45
using System.Threading.Tasks;
using UnityEngine;
using UnityEngine.UI;
 
public class ThreadManager : MonoBehaviour
{
    public Button StartButton;
 
    public int ThreadNumber;
 
    public int TotalCount = 100;
 
    [HideInInspector]
    public ObjectPool<int> NumberPool = new ObjectPool<int>();
 
    // Start is called before the first frame update
    void Start()
    {
        for (int i = 0; i < TotalCount; i++)
        {
            NumberPool.Push(i + 1);
        }
 
        StartButton.onClick.AddListener(()=> {
            ThreadStart();
        });        
    }
 
    private void ThreadStart()
    {
        for (int i = 0; i < ThreadNumber; i++)
        {
            Work wk = new Work(i+1this);
            Task t = Task.Factory.StartNew(() => {
                wk.Run();
            });
            //t.Wait();
        }
    }
 
    public void WriteConsole(int tNum, int number)
    {
        Debug.Log($"Thread Number:{tNum} CurrentNumber:{number}");
    }
}
cs

 

StartButton : 위에서 만든 버튼의 인스턴스를 등록하여 Thread를 시작해 줄 것 입니다. 

ThreadNumber : Thread를 2개가 아니라 n개 생성했을 때 어떻게 동작하는지 실험해 보기 위해서 변수로 만들어 둡니다. 

TotalCount : Object Pool 에 들어갈 아이템 갯 수 입니다. 

NumberPool : 앞서 만들어 주었던 Object Pool 클래스로 다음과 같이 만들어 주고 인스펙터에서는 건드릴 일이 없으니 숨겨 줍니다. 

 

Start() 함수 : NumberPool에 Total Count만큼 아이템을 생성해 줍니다.

                 Button Click을 하게 되면 Thread를 시작할 수 있도록 만들어 줍니다. 

 

ThreadStart() : ThreadNumber의 수 만큼 쓰레드를 생성하고

                   Work 인스턴스를 각각 만들어서 무언가를 수행하게 합니다.

 

// t.wait(); 이 부분을 제가 주석 달았는데요 주석을 풀고도 한번 해보세요.

// 어떤 결과가 나올지는 직접 확인해 보시면 알 것입니다. 

 

WriteConsole() : 결과를 확인해 줄 출력 함수도 하나 만들어 줍니다. 

 

 

 

 

3. 다시 유니티 에디터로 돌아 갑니다. 

 

4. 이번에는 Work 라는 이름으로 스크립트를 생성 합니다. 

 

 

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class Work
{
    int tNum;
 
    private ThreadManager ThreadManager;
 
    public Work(int _tNum, ThreadManager manager)
    {
        tNum = _tNum;
        ThreadManager = manager;
    }
 
    public void Run()
    {
        while(ThreadManager.NumberPool.Count > 0)
        {
            int n = ThreadManager.NumberPool.Pop();
            ThreadManager.WriteConsole(tNum, n);
        }
    }    
}
cs

 

Run() 함수를 보면 

object pool에서 하나씩 꺼내올 때마다 자신의 Thread Number 와 몇 번째 아이템인지를 출력할 수 있도록 코드를 작성 하였습니다. 

 

 

5. 이제 다시 유니티 에디터로 돌아가서 프로그램을 Run 하고 버튼을 클릭하게 되면 console 창에 다음과 같은 결과가 나옵니다.

 

 

2개의 쓰레드가 1~100 까지의 숫자를 중복없이 하나씩 꺼내 온 것을 확인할 수 있습니다. 

 

 

보시다 시피 여기서 유니티가 해준 일은 캔버스에 버튼 생성해서 로직을 실행 시킨 일 밖에는 없습니다. 

 

핵심적인 내용은 멀티 쓰레드를 사용하는 하나의 방법론을 코드적으로 풀어 나갔다는 것 입니다. 

 

 

주의 하실 점은 실전에서 사용하실 때 work 클래스에서 복잡한 로직을 수행하게 되면 유니티에서 프레임 드랍이 발생할 수도 있습니다. 

 

그런 점도 염두해 두면서 여러가지 방식으로 테스트 해보시면 좋을 것 입니다. 

 

혹은 자신만의 다른 방법을 고민해 보는 것도 좋을 것 같습니다. 

 

 

 

git hub 링크

 

https://github.com/Helloezzi/multithread_in_unity

 

Helloezzi/multithread_in_unity

Contribute to Helloezzi/multithread_in_unity development by creating an account on GitHub.

github.com

 

 

 

#멀티쓰레드c# 

반응형