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++ 쪽에서 처리하는 것이 너무 많다는 것이다. 뭔가 루아스럽지않다고 해야 하나? 루아스러운게 뭔지도 잘 모르겠다만 어쨌든 찝찝하다는 느낌을 버릴 수 없다.