사용자 도구

사이트 도구


kb:luapreemtivethreading

Lua Preemtive Threading

npc = create_npc("Bob");
npc:set_room("main");
npc:say("Hello everybody in the room!");
 
SLEEP(1000);  -- sleep for one second
npc:say("Hello again!!");
 
SLEEP(1000);  -- sleep for another second
npc:say("I'm being really annoying now!!");

“SLEEP(1000)“이라는 구문에서 의도한 바는 루아 스레드 실행을 멈추고, C/C++ 쪽으로 실행 컨트롤을 넘겨준 다음, 1초 후에 다시 루아 스레드를 실행하라는 것이다. 하지만 단순히 C 쪽의 sleep 함수를 저기다 집어넣으면, 루아 스레드가 C/C++ 쪽으로 컨트롤을 돌려주는 것이 아니라, 프로그램 전체가 1초 동안 먹통이 되버린다.

기본적으로 루아 스레드는 비선점형(non-preemtive)이기 때문에, 언어 차원에서 저런 함수를 지원하지는 않는다. 그렇다고 스크립트에다 온통 yield 떡칠하는 것도 피하고 싶다. 어떻게 하면 될까…

코루틴을 이용한 구현

코루틴을 이용해 선점형 스레딩을 흉내낼 수는 있다.

npc:set_room("main");
npc:say("Hello everybody in the room!");
 
SLEEP(1000);  -- sleep for one second
npc:say("Hello again!!");
 
SLEEP(1000);  -- sleep for another second
npc:say("I'm being really annoying now!!");

위 코드에서 쓰이는 SLEEP 함수는 다음과 같이 정의한다.

function SLEEP(duration)
    coroutine.yield(duration)
end

즉 SLEEP 호출이 일어나면 coroutine.yield를 수행하되, 몇 초 후에 스크립트의 실행을 재개해야 하는지를 C/C++ 쪽으로 넘긴다.

C/C++ 쪽에서는 lua_newthread 함수를 이용해 스레드를 생성하고, on_npc_create 함수를 실행할 스레드 메인 함수로 지정한 다음, lua_resume 함수를 통해 실행을 재개한다. lua_resume의 반환값에는 3가지 경우가 있다.

실행 리턴값 스택에 존재하는 값
끝난 경우 0 스레드 메인 함수가 반환한 값
중단된 경우 LUA_YIELD (1) lua_yield 함수에다 넘긴 인수들
에러가 생긴 경우 not 0 or 1 에러 메시지

:!: 예전 버전에서는 끝난 경우 및 중단된 경우 모두 0이 아닌 값을 반환했었던 걸로 기억하는데, 어느샌가 LUA_YIELD라는 값이 생겼다. 은근슬쩍 바뀌는 거 짜증나…

lua_State* prepare(lua_State* L, const char* filename)
{
    lua_State* coroutine = lua_newthread(L);
    if (luaL_loadfile(coroutine, filename))
    {
        // 파싱 실패! 여기서 에러 처리...
        ...
        return NULL;
    }
 
    return coroutine;
}
 
enum ResultCode
{
    FINISHED,
    SUSPENDED,
    ERROR_OCCURED
}
 
std::pair<ResultCode, int> execute(lua_State* coroutine)
{
    int result = lua_resume(coroutine, 0);
    if (result == 0) // 실행 완료.
    {
        return std::make_pair<ResultCode, int>(FINISHED, 0);
    }
    else if (result != LUA_YIELD) // 실행 중단.
    {
        int duration = 0;
        int duration = (int)lua_tonumber(coroutine, -1);
        lua_pop(coroutine, 1);
        return std::make_pair<ResultCode, int>(SUSPENDED, duration);
    }
    else // 에러 발생.
    {
        return std::make_pair<ResultCode, int>(ERROR_OCCURED, -1);
    }
}

execute 함수의 반환값에 따라, C/C++ 쪽에서 처리를 해줘야한다. 만일 기다려야 한다면, 폴링이 되었든, 타이머를 이용하든, 주어진 시간만큼 기다린 후에, 다시 execute를 호출하면 된다. 언젠가는 완료되겠지…

int main()
{
    ...
 
    lua_State* t = prepare(L, "test.lua");
    int next_time = 0;
 
    while (t != NULL)
    {
        if (next_time < get.current.time())
        {
            std::pair<ResultCode, int> result = execute(t);
            if (result.first == SUSPENDED) // 중단 처리.
            {
                // 주어진 값만큼 시간이 지난 후에 다시 실행한다.
                next_time = get.current.time() + result.second;
            }
            else if (result.first == FINISHED) // 완료 처리.
            {
                t = NULL;
            }
            else // 에러 처리.
            {
                t = NULL;
            }
        }
    }
 
    ...
 
    return 0;
}

이 방법의 단점은 C/C++ 쪽에서 처리하는 것이 너무 많다는 것이다. 뭔가 루아스럽지않다고 해야 하나? 루아스러운게 뭔지도 잘 모르겠다만 어쨌든 찝찝하다는 느낌을 버릴 수 없다.

훅을 이용한 구현

kb/luapreemtivethreading.txt · 마지막으로 수정됨: 2014/11/06 17:11 (바깥 편집)