티스토리 뷰

Unity3d

유니티에서 쓰레드

arga32wr32 2016. 3. 20. 22:04

유니티에서 쓰레드

Unity3d 엔진은 단일 쓰레드를 사용한다. 다중 쓰레드를 사용함으로써 발생하는 여러가지 경합조건등을 신경쓰지 않아서 좋다. 하지만 게임을 만들다보면 어쩔 수 없이 쓰레드가 필요한 경우들이 생긴다. 멀티 쓰레드를 꼭 사용하고 싶다면 사용할 수있다. 하지만 유니티에서는 쓰레드 대신 코루틴 사용을 권장한다. 멀티 쓰레드를 사용하는 경우, 메인 쓰레드 이외의 쓰레드에서 리소스에 접근하려고 하면 에러가 발생한다. 코루틴은 이런 문제가 발생하지 않고 사용할 수 있다. 코루틴에서 Update() 처럼 주기적으로 또는 일정 시간, 프래임 등의 간격을 지정하여 일을 처리할 수 있다. 쓰레드 처럼 여러개의 코루틴을 한꺼번에 동작시키는 일도 가능하다. 하지만 쓰레드 처럼 중간에 UI 접근시 에러를 발생시키지는 않는다. 그러니까 코루틴을 적극 활용해야 한다.


프로파일러를 통해 메인쓰레드가 열심히 일하고 있는 모습이 보인다. 그 아래 랜더링 쓰레드 워커 쓰레드 들이 보인다.


메인 쓰레드 이외의 쓰레드에서 리소스에 접근시 에러 내용

get_enabled can only be called from the main thread.

Constructors and field initializers will be executed from the loading thread when loading a scene.

Don't use this function in the constructor or field initializers, instead move initialization code to the Awake or Start function.


어쩔 수 없이 멀티 쓰레드를 사용하는 경우

게임을 만드는 도중 여러 플러그인들을 사용하게 된다. 광고,결제,통계,소셜 등등.. 많은 플러그인들의 서비스가 돌아갈 때 각자의 쓰레드를 사용하는 경우가 많다. 또는 게임서버와 소켓 통신을 하는 경우 소켓 라이브러리를 사용해야한다. 게임서버와 소켓 라이브러리를 사용하여 소켓통신하는 경우를 예로 들어보자. 소켓 라이브러리는 서버와 통신하는 로직을 별도의 쓰레드를 돌려서 사용한다. 소켓을 통해 게임서버와 데이터를 주고 받는 과정이 이 쓰레드에서 처리된다. 그런데 주고받은 데이터를 UI에 표시하거나 게임에서 사용하려고 할 때 에러가 발생한다. 유니티의 UI나 리소스는 메인 쓰레드 이외에 다른 쓰레드에서는 사용할 수 없기 때문이다.




구현 예제

클라이언트

/*
 * 서버에 데이타를 요청하고 응답받아 UI에 표시
*/

using UnityEngine;
using UnityEngine.UI;

public class Client : MonoBehaviour 
{
    //Inspector
    public Text textUI;         //데이타를 표시할 UI
    //Concrete classes
    private SocketLib socket;   //서버와 통신하는 소켓라이브러리

	void Awake () 
    {
        socket = new SocketLib();

        RequestData();
	}

    //서버에 데이타 요청
    private void RequestData()
    {
        ClientDele requestDele = new ClientDele(ResponseData);

        socket.Request(requestDele);
    }

    //서버에서 응답 받음
    public void ResponseData(string data)
    {
        textUI.text = data;
    }
}

소켓라이브러리

/*
 * 서버와 통신하는 소켓 라이브러리 흉내
*/

using System.Threading;

public delegate void ClientDele(string data);

public class SocketLib
{
    //서버 작업 처리 동안 대기할 쓰레드
    private Thread thread;
    
    private ClientDele dele;

    public SocketLib()
    {
        thread = new Thread(new ThreadStart(Run));
    }

    //서버에 요청
    public void Request(ClientDele dele)
    {
        this.dele = dele;
        thread.Start();
    }

    //서버 작업 진행
    void Run()
    {
        //서버 작업 시작
        string result;

        Thread.Sleep(3000);

        //서버 작업 종료
        result = "This is result data.";

        Response(result);
    }

    //응답 받음
    public void Response(string result)
    {
        //결과 클라이언트에게 전달
        dele(result);
    }
}

설명

문제가 되는 부분만 집중하여, 클라이언트와 소켓 라이브러리를 간략화 하였다. 클라이언트에서는 소켓 라이브러리를 생성하고 바로 데이타를 소켓을 통해 요청한다. 소켓라이브러리에서는 실제 서버처리는 없고 쓰레드를 통한 작업지연을 흉내내었다. 소켓 라이브러리에서는 데이타를 다시 클라이언트에 돌려줄 때 3초의 대기시간을 가졌다. 그리고 생성한 값을 클라이언트에게 돌려줬다. 클라이언트는 절달 받은 데이타를 UI에 출력했다. 에러는 UI에 데이타를 출력하는 부분에서 발생한다.하지만 데이타를 출력하는 부분이 문제가 아니다. 문제는 쓰레드를 통해 UI에 접근하려 한것이다. 


해결책

이 문제는 유니티 뿐만아니라 안드로이드,아이폰 등 많은 플렛폼에서 똑같이 발생한다. 에러의 원인은 대개 메인 쓰레드 이외의 쓰레드가 감히 메인쓰레드의 허락 없이 UI에 접근하려했기 때문이다. 해결책도 플랫폼마다 비슷하다. 소켓 라이브러리 쓰레드가 UI 수정에 접근할 수 없으니 메인 쓰레드에게 허락을 받거나, UI를 변경 해달라고 메인 쓰레드에게 요청하는 것이다. 구체적인 방법으로는, Queue를 하나 만들고 이것을 메인쓰레드와 이외의 쓰레드가 함께 공유하는 것이다. 

첫번째, 소켓 라이브러리는 서버로 부터 받은 데이타를 이 Queue에 push 한다. (받을 때마다 push하여 쌓아둠)

두번째, 유니티의 메인 쓰레드가 이 Queue의 크기를 주지적으로 체크하여 데이타가 있으면 그 데이타를 pop하여 UI를 변경하는 등의 행위를 한다. 

이렇게 간단하게 서로 데이타를 직접적으로 주고받지 않고 Queue를 사용하는 것이다. 유니티에서 Queue를 주기적으로 탐색하기 위해서는 Update()에서 Queue를 체그하거나 이것이 부담스럽다면 코루틴을 사용해도 좋다. Update()나 코루틴은 유니티 메인쓰레드에서 관리하기 때문에 데이타 접근시 에러가 발생하지 않는다.




클라이언트
/*
 * 클라이언트
 * 서버에 데이타를 요청하고 응답받아 UI에 표시
*/

using System.Collections;
using UnityEngine;
using UnityEngine.UI;

public class Client : MonoBehaviour 
{
    //Inspector
    public Text textUI;         //데이타를 표시할 UI
    //Concrete classes
    private SocketLib socket;   //서버와 통신하는 소켓라이브러리
    private Postbox postbox;    //메세지 큐를 관리하는 우편함

	void Awake () 
    {
        socket = new SocketLib();
        postbox = Postbox.GetInstance;
        
        //큐 탐색 시작
        StartCoroutine(CheckQueue());
        //데이타 요청 시작
        RequestData();
	}

    //서버에 데이타 요청
    private void RequestData()
    {
        socket.Request();
    }

    //서버에서 응답 받음
    public void ResponseData(string data)
    {
        textUI.text = data;
    }

    //큐를 주기적으로 탐색
    private IEnumerator CheckQueue()
    {
        //1초 주기로 탐색
        WaitForSeconds waitSec = new WaitForSeconds(1);

        while (true)
        {
            //우편함에서 데이타 꺼내기
            string data = postbox.GetData();

            //우편함에 데이타가 있는 경우
            if (!data.Equals(string.Empty))
            {
                //데이타로 UI 갱신
                ResponseData(data);
                yield break;
            }

            yield return waitSec;
        }
    }
}
포스트 박스
using System.Collections.Generic;

public class Postbox 
{
    //싱글턴 인스턴스
    private static Postbox instance;
    //싱글턴 인스턴스 반환
    public static Postbox GetInstance
    {
        get
        {
            if (instance == null)
                instance = new Postbox();

            return instance;
        }
    }

    //데이타를 담을 큐
    private Queue<string>  messageQueue;

    private Postbox()
    {   //큐 초기화
        messageQueue = new Queue<string>();
    }

    //큐에 데이타 삽입
    public void PushData(string data)
    {
        messageQueue.Enqueue(data);
    }

    //큐에있는 데이타 꺼내서 반환
    public string GetData()
    {
        //데이타가 1개라도 있을 경우 꺼내서 반환
        if (messageQueue.Count > 0)
            return messageQueue.Dequeue();
        else
            return string.Empty;    //없으면 빈값을 반환
    }
}
소켓라이브러리
/*
 * 서버와 통신하는 소켓 라이브러리 흉내
*/

using System.Threading;

public class SocketLib
{
    //서버 작업 처리 동안 대기할 쓰레드
    private Thread thread;
    
    public SocketLib()
    {
        thread = new Thread(new ThreadStart(Run));
    }

    //서버에 요청
    public void Request()
    {
        thread.Start();
    }

    //서버 작업 진행
    void Run()
    {
        //서버 작업 시작
        string result;

        Thread.Sleep(3000);

        //서버 작업 종료
        result = "This is result data.";

        Response(result);
    }

    //응답 받음
    public void Response(string result)
    {
        //결과 데이타를 큐에 넣기
        Postbox.GetInstance.PushData(result);
    }
}

설명

포스트 박스를 추가하였다. 기존에 클라이언트와 소켓 라이브러리가 데이터를 직접 전달 받았지만 이제 중간에 큐를 통해 데이터를 전달하게 되었다. 직접 전달할 필요가 없으니 델리게이트는 제거했다. 클라이언트에서 소켓라이브러리로 데이터를 요청하는 것은 큐를 통하지 않아도 된다. 리소스나 UI에 접근하는 일과 상관 없기 때문이다. 하지만 소켓라이브러리에서 데이타를 클라이언트에게 전달하는 방법은 중간에 큐를 통하는 것으로 바뀌었다. 소켓라이브러리는 서버에서 받은 데이타를 큐에만 쌓는 것으로 역할은 끝이다. 클라이언트가 가져다 쓰건 말건 상관하지 않는다. 마치 등기 우편과 일반우편의 차이같다. 등기 우편은 집배원이 수신자의 집에 찾아가 수신자에게 직접 편지를 전달한다. 하지만 일반 우편은 집배원이 수신자의 집 우편함에 편지만 두고 떠난다. 수신자가 우편함을 열고 편지를 찾아가야 한다. 집배원은 편지를 넣고 떠난뒤 수신자가 편지를 찾아가건 말건 상관하지 않는다. 수신자는 주기적으로 우편함을 열어서 편지가 왔는지 체크해야한다. 위의 예제에서는 코루틴을 사용하여 1초 주기로 큐를 검사하도록 하였다. 코루틴 말고 Update()을 사용해도 괜찮다. 둘 모두 에러를 발생하지 않는다.


유니티 예제 다운로드

ThreadInUnity.unitypackage


댓글