ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [Lua] 코루틴 (Coroutine)
    Lua 2025. 2. 25. 19:13

    코루틴은 멀티스레딩에서의 스레드와 유사한 개념입니다.

    스레드처럼 실행 흐름의 한 줄기로서 각 코루틴은 독립된 스택, 지역 변수, 제어를 가지며, 전역 변수 등의 공유 자원은 모든 코루틴이 공유합니다.

    하지만 여러 스레드가 병렬적으로 실행되는 멀티스레드 환경과는 달리, 코루틴은 한 번에 한 코루틴만 실행되며, 특히 루아의 코루틴은 명시적으로 중지시켜야 중지된다는 차이점이 있습니다.

     

    이번 글에서는 루아에서 코루틴을 사용하는 방법, 간단한 예제 몇 가지를 살펴보도록 하겠습니다.

     


     

    0. 코루틴 생성하기

     

    더보기

    코루틴과 관련된 함수는 coroutine 테이블에 있습니다.

    코루틴은 다음과 같이 생성할 수 있습니다.

    co = coroutine.create(function() print("Hello coroutine") end)

    coroutine의 create함수는 실행할 함수를 인자로 받아 코루틴을 생성합니다.

    예제는 익명 함수를 전달했지만, 실행할 다른 함수가 정의되어 있을 경우 그 함수를 전달할 수 있습니다.

     

    생성된 코루틴은 thread 타입의 값으로 표현됩니다.

    print(co)
    -- thread: 0000000000

     

    1. 코루틴의 상태

     

    더보기

    코루틴은 suspended(중지), running(실행 중), dead(종료), normal(일반)의 4가지 상태를 가질 수 있습니다.

    코루틴의 상태는 status함수를 사용해서 확인할 수 있습니다.

    co = coroutine.create(function() print("Hello coroutine") end)
    
    print(coroutine.status(co))
    -- suspended

    최초로 생성된 코루틴은 suspended 상태입니다.

     

    코루틴은 resume함수를 사용하여 실행시킬 수 있습니다.

    co = coroutine.create(function() print("Hello coroutine") end)
    coroutine.resume(co)
    
    print(coroutine.status(co))
    -- Hello coroutine
    -- dead

    실행된 코루틴이 문자열을 출력 후 종료되어 dead상태로 전이한 것을 볼 수 있습니다.

     

    실행중인 코루틴이 yield함수를 호출할 경우, 해당 코루틴을 중지하고 suspended상태로 전이합니다.

    co = coroutine.create(function() 
            for i = 1, 10 do
                print("Hello coroutine ", i)
                print(coroutine.status(co))
                coroutine.yield()
            end
        end)
    coroutine.resume(co)
    coroutine.resume(co)
    
    print(coroutine.status(co))
    -- Hello coroutine 1
    -- running
    -- Hello coroutine 2
    -- running
    -- suspended

    코루틴에 전달된 함수는 10회의 반복문을 실행하는 함수입니다.

    하지만 각 반복마다 yield가 호출되어, resume호출 한 번에 한 번의 반복문만 실행하는 것을 볼 수 있습니다.

    또한 yield가 호출되기 전, 코루틴이 자기 자신의 상태를 확인할 경우 running상태인 것을 볼 수 있습니다.

    yield가 호출될 경우 코루틴이 중지되므로 상태를 확인할 경우 suspended인 것 또한 볼 수 있습니다.

     

    normal 상태는 활성화 된 코루틴이 두 개 이상일 때, 제어가 없는 코루틴의 상태를 의미합니다.

    co1 = coroutine.create(function() 
        for i = 1, 2 do
            print("Hello coroutine ", i)
            print(coroutine.status(co2))
        end
    end)
    
    co2 = coroutine.create(function()
        coroutine.resume(co1)
        for i = 1, 2 do
            print("Hello coroutine2 ", i)
            print(coroutine.status(co1))
        end
    end)
    
    coroutine.resume(co2)
    
    -- Hello coroutine 	1
    -- normal
    -- Hello coroutine 	2
    -- normal
    -- Hello coroutine2 	1
    -- dead
    -- Hello coroutine2 	2
    -- dead

    두 개의 코루틴(co1, co2)를 생성했습니다.

    각각의 코루틴은 반복문 내부에서 다른 코루틴의 상태를 출력합니다. (co1은 co2의 상태를 출력합니다)

    추가로, co2는 co1을 실행시킵니다.

    위와 같은 코드 구성에서 co2를 실행시킬 경우 다음과 같은 순서로 진행됩니다.

    1. co2가 실행됩니다.

    2. co2의 반복문에 진입하기 이전, co1이 실행됩니다. 이 때, 제어는 co1로 넘어갑니다.

    3. co1이 실행됩니다. 이 때, co2에서 yield를 호출하지 않았으므로 co2의 상태는 normal 입니다.

    4. co1의 실행이 종료되고, co2로 제어가 돌아옵니다.

    5. co2가 실행됩니다. 이 때, co1은 이미 실행이 종료되었기 때문에 co1의 상태는 dead입니다.

     

    각각의 코루틴의 상태에 대해 알아보았습니다.

    요약하면 다음과 같습니다.

    - suspended : 코루틴이 (일시)중지된 상태입니다. 코루틴이 생성된 직후, 혹은 코루틴이 yield를 호출한 이후의 상태입니다.

    - running : 코루틴이 실행되고 있는 상태입니다. 실행 중인 코루틴에서 자신의 상태를 status로 확인할 때 볼 수 있습니다.

    - normal : 코루틴이 실행되고 있으나, 다른 코루틴이 실행되어 제어를 상실한 상태입니다. 코루틴이 다른 코루틴을 실행했을 때, 호출자의 상태가 될 수 있습니다.

    - dead : 코루틴이 종료된 상태입니다. 코루틴에 전달한 함수의 실행이 종료되었을 때의 상태가 될 수 있습니다.

     

    2. resume과 yield

     

    더보기

    resume함수는 코루틴을 실행시키는 함수입니다.

    resume의 첫 인자는 실행시킬 코루틴의 thread값을 가지고 있는 변수입니다.

    이후 추가로 인자를 입력할 수 있는데, 추가로 전달되는 인자는 코루틴에 지정된 함수로 전달됩니다.

    co = coroutine.create(function(arg1, arg2)
        print(arg1, arg2)
    end)
    
    coroutine.resume(co, "Hello,", "coroutine")
    -- Hello, coroutine

    co는 인자 두 개를 받아 출력하는 함수의 코루틴입니다.

    resume함수에 인자를 넣어 전달하는 것으로 코루틴의 함수에 인자가 전달되는 것을 볼 수 있습니다.

     

    resume함수의 반환값은 다음과 같은 값이 될 수 있습니다.

    - 코루틴의 실행 결과 (bool) : 코루틴이 정상적으로 실행되었는지의 여부가 bool값으로 반환됩니다.

    - yield함수의 인자값 : yield함수의 인자는 resume함수의 반환값으로 전달됩니다.

    - 코루틴의 return값 : 코루틴에 지정한 함수의 return값은 resume함수의 반환값으로 전달됩니다.

    co = coroutine.create(function(arg1, arg2)
        print(arg1, arg2)
        coroutine.yield("Reusme")
        return "Return"
    end)
    
    print(coroutine.resume(co, "Hello,", "coroutine"))
    print(coroutine.resume(co, "Hello2,", "coroutine"))
    -- Hello, coroutine
    -- true Resume
    -- true Return

    두 번째 resume에 전달한 값은 코루틴에는 전달되었지만, 이미 출력이 완료되었기 때문에 출력되지 않는 것을 볼 수 있습니다.

    각각 제어 순서에 따라 yield의 값, return의 값이 반환된 것을 볼 수 있습니다.

     

    3. 코루틴 예제 : 생산자 - 소비자

     

    더보기

    파일을 읽어들이는 함수와, 읽어들인 값을 가공하는 함수를 생각해보겠습니다.

    간단하게 다음과 같은 구성을 생각해볼 수 있습니다.

    function producer()
        while true do
            local r = io.read()
            send(r)
        end
    end
    
    function consumer()
        while true do
            local w = recieve()
            -- Work
        end
    end

    예제의 코드가 무한반복으로 돌아가는 것 보다, producer와 consumer가 각각 자신의 서비스를 가지고 있다는 점에 주목해주세요.

    각각의 서비스가 호출하는 sned와 recieve는 읽어들인 값을 다른 메모리로 이동시키고, 꺼내는 역할을 하는 함수가 될 것입니다.

    위 코드 구성은 어떤 서비스를 주도적으로 실행시킬지 결정해야 합니다.

    그 과정에서 한 코드가 다른 코드에 종속적이게 구조를 변경하는 것 등의 방법을 생각해볼 수 있으나, 각 서비스의 실행 속도 등을 고려할 경우 이것이 쉽지 않음을 예상할 수 있습니다.

     

    코루틴은 위와 같은 상황에서 적절한 해법이 될 수 있습니다.

    resume과 yield의 관계를 떠올려보면, 각각이 데이터와 제어를 전달할 수 있습니다. 

    이 경우, 다음과 같이 코드를 작성할 수 있습니다.

    producer = coroutine.create(function()
        while true do
            local r = io.read()
            coroutine.yield(r)
        end
    end)
    
    function consumer()
        while true do
            local status, w = coroutine.resume(producer)
            -- Work
        end
    end

    producer가 코루틴이 되었고, send와 recive가 각각 yield와 resume이 되었습니다.

    각각 호출 시 제어를 서로 전달하여, 데이터를 읽고 처리하는 과정을 반복합니다.

     

    위 예제는 생산자 (producer)가 코루틴이 되어, 소비자 (consumer)가 주 반복문이 됩니다.

    consumer는 데이터가 필요할 때 producer를 가동시켜 데이터를 받아옵니다.

    이를 소비자 주도 설계 (Consumer-driven design)이라 합니다.

    만약 관계가 바뀐다면 생산자 주도 설계(Producer-driven design)이 됩니다.

     

    4. 코루틴 예제 : 파이프와 필터

     

    더보기

    위 문단의 생산자 - 소비자 설계를 확장하여 필터라는 개념을 추가할 수 있습니다.

    필터는 소비자이자 생산자의 역할을 수행합니다.

    예제를 살펴보도록 하겠습니다.

    function receive(producer)
        local status, value = coroutine.resume(producer)
        return value
    end
    
    function producer()
        return coroutine.create(function()
            while true do
                local r = io.read()
                coroutine.yield(r)
            end
        end)
    end
    
    function filter(producer)
        return coroutine.create(function()
            for line = 1, math.huge do
                local r = receive(producer)
                -- Work
                coroutine.yield(r)
            end
        end)
    end
    
    function consumer(producer)
        while true do
            local status, w = coroutine.resume(producer)
            -- Work
        end
    end
    
    
    consumer(filter(producer()))

    예제를 각각 분리해서 살펴보도록 하겠습니다.

     

    우선 producer입니다.

    기존의 producer와 동일하게 데이터를 읽어들이고 그 값을 전달합니다.

    이전 코드와 다른점은 코루틴을 반환하는 점 입니다. 

     

    filter는 새로 추가된 레이어입니다.

    filter또한 코루틴을 생성하여 반환합니다.

    생성되는 코루틴은 filter에 전달된 producer를 실행시킵니다.(receive를 호출함으로써)

    이후 filter에서 필요한 역할을 수행하고 (주석의 Work) 값을 전달합니다.

     

    consumer는 producer를 인자로 받게 되었습니다.

    받은 producer로부터 값을 받아와 자신의 역할을 수행합니다.

     

    위와 같은 구성을 예제의 마지막 줄과 같이 실행합니다.

    객체의 초기화가 완료되었다고 가정하면, 마지막 코드는 다음과 같은 순서로 실행됩니다.

    1. consumer가 receive를 호출합니다. 

    2. consumer의 receive는 filter를 실행합니다.

    3. filter가 receive를 호출합니다.

    4. filter의 receive는 producer를 실행합니다.

    5. producer가 실행되어 데이터가 생성되고, 생성된 값이 filter를 거쳐 consumer로 전달됩니다.

     

    5. 코루틴 예제 : 반복자

     

    더보기

    코루틴은 반복문의 반복자에서도 사용할 수 있습니다.

    위 문단의 생산자 - 소비자의 경우와 유사합니다.

    반복자를 생산자로, 반복자가 호출되는 반복문을 소비자로 볼 수 있겠습니다.

     

    다음과 같이 배열을 통해 순열을 생성하는 함수를 가정해보겠습니다.

    function printResult(arr)
        for i = 1, #arr do
            io.write(arr[i], " ")
        end
        io.write("\n")
    end
    
    function permu_gen(arr, n)
        n = n or #arr
        if n<=1 then
            coroutine.yield(arr)
        else
            for i = 1, n do
                arr[n], arr[i] = arr[i], arr[n]
                permu_gen(arr, n-1)
                arr[n], arr[i] = arr[i], arr[n]
            end
        end
    end
    
    permu_gen({1,2,3,4})
    -- 2 3 4 1
    -- 3 2 4 1
    -- ...
    -- 1 2 3 4

    permu_gen은 배열의 앞쪽 n개의 원소를 이용해 순열을 생성하는 생성기 함수입니다.

     

    생성기는 다음과 같이 사용할 수 있습니다.

    function permutation(arr)
        local co = coroutine.create(function () permu_gen(arr) end)
        return function ()
            local status, arr = coroutine.resume(co)
            return arr
        end
    end

    permutation함수는 반복자 클로저를 반환합니다.

    클로저는 내부에서 생성기를 실행시키며 순열을 하나씩 반환합니다.

     

    이제 일반 for문에서 다음과 같이 배열을 순회할 수 있습니다.

    for permu in permutations({"a","b","c"}) do
        printResult(permu)
    end
    -- b c a
    -- c b a
    -- ...
    -- a b c

     

    6, coroutine.wrap

     

    더보기

    위 문단의 예제들에서는 코루틴을 생성할 때 create함수를 사용했습니다.

    하지만 Lua에서는 코루틴을 생성하는 한 가지 방법을 추가로 제공합니다.

     

    wrap함수가 그것으로, 다음과 같이 사용할 수 있습니다.

    co = coroutine.wrap(function ()
        print("Hi")
    end)
    
    co()
    -- Hi
    print(co)
    -- function: 00000000000

    create는 코루틴의 스레드를 반환하지만, wrap은 코루틴을 재개시키는 함수를 직접적으로 반환합니다.

    타입 또한 function임을 볼 수 있습니다.

    따라서 create로 생성한 코루틴을 resume으로 실행시키는 반면, wrap은 직접적으로 호출하여 사용합니다.

     

    차이점은 추가로 존재합니다.

    create와는 달리 wrap은 코루틴의 상태를 확인할 수 없고, 오류 코드 등을 반환하는 기능도 없습니다.

    전체적으로 create를 간소화 한 느낌입니다.

     

    위 문단의 permutation을 wrap을 사용할 경우 코드가 간결해집니다.

    function permutation(arr)
        return coroutine.wrap(function () permu_gen(arr) end)
    end

    직접적으로 호출 함수를 반환하므로, 반복문 등지에서 활용할 경우 간결한 코드 구성을 할 수 있습니다.

     


     

    코루틴의 사용법 및 코루틴을 활용한 예제를 알아보았습니다.

    코루틴은 서론에서 언급한 것 처럼 병렬 실행이 되지 않습니다.

    하지만 여러 코루틴을 잘 구성할 경우, 단일 스레드로 실행하는 것 보다 더 효율적입니다.

    이에 관하여는 차후 다른 글로 찾아뵙도록 하겠습니다.

     

    이번 글이 도움이 되셨기를 바랍니다.

    감사합니다.

    'Lua' 카테고리의 다른 글

    [Lua] LuaRocks (Lua Package Manager) 설치하기 (Windows)  (0) 2025.03.03
    [Lua] Lua Build for Windows  (0) 2025.02.28
    [Lua] 반복자 (Iterator)와 일반 for문  (0) 2025.02.19
    [Lua] 함수 (2)  (0) 2025.02.17
    [Lua] 함수 (1)  (0) 2025.02.13

    댓글

Designed by Tistory.