Lua コルーチン(coroutine)


コルーチンとは?

Lua のコルーチンはスレッドと比較的似ています。独立したスタック、独立したローカル変数、独立した命令ポインタを持ち、同時に他のコルーチンとグローバル変数やその他のほとんどのものを共有します。

コルーチンは特殊なスレッドと理解でき、その実行を一時停止・再開できるため、非プリエンプティブなマルチタスク処理を可能にします。

コルーチンは非常に強力な機能ですが、使い方は複雑でもあります。

基本構文

コルーチンはcoroutineモジュールによってサポートされています。

コルーチンを使用すると、関数内でcoroutine.create新しいコルーチンオブジェクトを作成し、使用してcoroutine.resumeその実行を開始します。コルーチンは呼び出すことでcoroutine.yield自らの実行を能動的に一時停止し、制御を呼び出し元に返します。

メソッド説明
coroutine.create()coroutine を作成し、coroutine を返します。引数は関数で、resume と組み合わせて使用すると関数呼び出しを起動します。
coroutine.resume()coroutine を再起動します。create と組み合わせて使用します。
coroutine.yield()coroutine を一時停止し、coroutine を一時停止状態に設定します。これは resume と組み合わせると多くの有用な効果が得られます。
coroutine.status()coroutine の状態を確認します。
注: coroutine の状態には dead、suspended、running の3つがあります。具体的にいつこのような状態になるかは、以下のプログラムを参照してください。
coroutine.wrap()coroutine を作成し、関数を返します。この関数を呼び出すと coroutine に入ります。create と同じ機能です。
coroutine.running()実行中の coroutine を返します。coroutine はスレッドであり、running を使用すると coroutine のスレッドIDを返します。

以下の例では、Lua コルーチンの使用方法を示します。

例

function foo()
    print("コルーチン foo の実行を開始")
    local value = coroutine.yield("foo の実行を一時停止")
    print("コルーチン foo の実行を再開、渡された値: " .. tostring(value))
    print(コルーチン foo の実行を終了)
end

-- コルーチンを作成
local co = coroutine.create(foo)

-- コルーチンを起動
local status, result = coroutine.resume(co)
print(result) -- 出力: foo の実行を一時停止

-- コルーチンの実行を再開し、値を渡す
status, result = coroutine.resume(co, 42)
print(result) -- 出力: コルーチン foo の実行を再開、渡された値: 42

上記の例では、foo という名前の関数をコルーチンとして定義しました。関数内で coroutine.yield を使用してコルーチンの実行を一時停止し、値を返しました。

。メインプログラムでは、coroutine.create を使用してコルーチンオブジェクトを作成し、coroutine.resume を使用してその実行を開始しました。

最初の coroutine.resume 呼び出し後、コルーチンは coroutine.yield の位置で一時停止し、値をメインプログラムに返します。次に、coroutine.resume を再度呼び出し、コルーチンが実行を再開するときの引数として値を渡します。

上記のコードを実行すると、出力結果は次のようになります。

协同程序 foo 开始执行
暂停 foo 的执行
协同程序 foo 恢复执行,传入的值为: 42
协同程序 foo 结束执行
nil

注意すべき点は、コルーチンの状態は coroutine.status 関数で取得でき、状態を確認することでコルーチンの実行状況(実行中、一時停止中、終了済みなど)を判断できることです。

以下の例では、上記の各メソッドの使い方を示します。

coroutine_test.lua ファイル

-- coroutine_test.lua ファイル
-- 新しいコルーチンオブジェクト co を作成し、コルーチン関数は渡された引数 i を出力します。
co = coroutine.create(
    function(i)
        print(i);
    end
)
-- coroutine.resume を使用してコルーチン co の実行を開始し、引数 1 を渡します。コルーチンが実行を開始し、出力は 1 になります。
coroutine.resume(co, 1)   -- 1

-- coroutine.status でコルーチン co の状態を確認すると、出力は dead で、コルーチンが実行を完了したことを示します。
print(coroutine.status(co))  -- dead
 
print("----------")

-- coroutine.wrap を使用してコルーチンラッパーを作成し、コルーチン関数を直接呼び出し可能な関数オブジェクトに変換します。
co = coroutine.wrap(
    function(i)
        print(i);
    end
)
 
co(1)
 
print("----------")
別のコルーチンオブジェクト co2 を作成し、そのコルーチン関数はループで数字 1 から 10 を出力し、ループが 3 のときに現在のコルーチンの状態と実行中のスレッドを出力します。
co2 = coroutine.create(
    function()
        for i=1,10 do
            print(i)
            if i == 3 then
                print(coroutine.status(co2))  --running
                print(coroutine.running()) --thread:XXXXXX
            end
            coroutine.yield()
        end
    end
)

-- coroutine.resume を連続して呼び出し、コルーチン co2 の実行を開始します。
coroutine.resume(co2) --1
coroutine.resume(co2) --2
coroutine.resume(co2) --3

-- coroutine.status でコルーチン co2 の状態を確認すると、出力は suspended で、コルーチンが実行を一時停止していることを示します。
print(coroutine.status(co2))   -- suspended
print(coroutine.running())
 
print("----------")

上記の例を実行した出力結果は次のとおりです。

1
dead
----------
1
----------
1
2
3
running
thread: 0x7fb801c05868    false
suspended
thread: 0x7fb801c04c88    true
----------

coroutine.running を見ればわかるように、coroutine は内部実装的にはスレッドです。

create で coroutine を作成するときは、新しいスレッドにイベントを登録することです。

resume を使用してイベントをトリガーすると、create した coroutine 関数が実行されます。yield に遭遇すると現在のスレッドを一時停止し、再び resume でイベントがトリガーされるのを待ちます。

次に、より詳細な例を分析します。

例

function foo (a)
    print("foo 関数の出力", a)
    return coroutine.yield(2 * a) -- 2*a の値を返す
end
 
co = coroutine.create(function (a , b)
    print("最初のコルーチン実行の出力", a, b) -- co-body 1 10
    local r = foo(a + 1)
     
    print("2回目のコルーチン実行の出力", r)
    local r, s = coroutine.yield(a + b, a - b)  -- a、b の値は最初のコルーチン呼び出し時に渡されます。
     
    print("3回目のコルーチン実行の出力", r, s)
    return b, "コルーチンを終了"                   -- b の値は2回目のコルーチン呼び出し時に渡されます。
end)
       
print("main", coroutine.resume(co, 1, 10)) -- true, 4
print("--区切り線----")
print("main", coroutine.resume(co, "r")) -- true 11 -9
print("---区切り線---")
print("main", coroutine.resume(co, "x", "y")) -- true 10 end
print("---区切り線---")
print("main", coroutine.resume(co, "x", "y")) -- cannot resume dead coroutine
print("---区切り線---")

上記の例を実行した出力結果は次のとおりです。

第一次协同程序执行输出    1    10
foo 函数输出    2
main    true    4
--分割线----
第二次协同程序执行输出    r
main    true    11    -9
---分割线---
第三次协同程序执行输出    x    y
main    true    10    结束协同程序
---分割线---
main    false    cannot resume dead coroutine
---分割线---

上記の例の説明は以下のとおりです。

  • resume を呼び出してコルーチンを起動します。resume 操作は成功すると true を返し、そうでなければ false を返します。
  • コルーチンが実行されます。
  • yield ステートメントまで実行されます。
  • yield がコルーチンを一時停止し、最初の resume が戻ります。(注: ここでの yield の戻り値は resume の引数です。)
  • 2回目の resume で再びコルーチンを起動します。(注: ここでの resume の引数のうち、最初の引数以外は yield の引数になります。)
  • yield が戻ります。
  • コルーチンは実行を継続します。
  • 使用しているコルーチンが実行を完了した後に resume メソッドを呼び続けると、cannot resume dead coroutine と出力されます。

resume と yield の組み合わせの強力な点は、resume がメインプログラムにあり、外部の状態(データ)をコルーチン内部に渡すことです。一方、yield は内部の状態(データ)をメインプログラムに返します。


プロデューサー・コンシューマー問題

ここで Lua のコルーチンを使用して、プロデューサー・コンシューマーという古典的な問題を解決します。

例

local newProductor

function productor()
     local i = 0
     while true do
          i = i + 1
          send(i)     -- 生産したアイテムをコンシューマーに送信する
     end
end

function consumer()
     while true do
          local i = receive()     -- プロデューサーからアイテムを受け取る
          print(i)
     end
end

function receive()
     local status, value = coroutine.resume(newProductor)
     return value
end

function send(x)
     coroutine.yield(x)     -- x は送信する値を表し、値を返した後、そのコルーチンを一時停止します。
end

-- プログラムを起動
newProductor = coroutine.create(productor)
consumer()

上記の例を実行した出力結果は次のとおりです。

1
2
3
4
5
6
7
8
9
10
11
12
13
……

スレッドとコルーチンの違い

スレッドとコルーチンの主な違いは、複数のスレッドを持つプログラムは複数のスレッドを同時に実行できますが、コルーチンは互いに協調して実行する必要があることです。

任意の指定された時点で実行されているコルーチンは1つだけであり、実行中のコルーチンは明示的に一時停止を要求された場合にのみ一時停止されます。

コルーチンは同期マルチスレッドに少し似ており、同じスレッドロックを待機する複数のスレッドはコルーチンに少し似ています。

主な違いは次のようにまとめられます。

  • スケジューリング方式: スレッドは通常、オペレーティングシステムのスケジューラによるプリエンプティブスケジューリングで、OS は異なるスレッド間で実行権を切り替えます。一方、コルーチンは非プリエンプティブスケジューリングであり、プログラマーが明示的に実行権の移行を制御します。

  • 並行性: スレッドは並行実行され、複数のスレッドが複数のプロセッサコア上で同時に実行されたり、タイムスライス方式で単一コア上で切り替えて実行されたりします。コルーチンは協調的であり、実行状態にあるコルーチンは1つだけで、他のコルーチンは現在のコルーチンが自ら実行権を放棄するのを待たなければなりません。

  • メモリ使用量: スレッドは通常、独立したスタックとコンテキスト環境が必要なため、スレッドの作成と破棄には余分なオーバーヘッドが伴います。一方、コルーチンは同じスタックとコンテキストを共有できるため、作成と破棄のオーバーヘッドは小さくなります。

  • データ共有: スレッド間ではメモリ空間を共有できますが、スレッドセーフティと同期の問題に注意する必要があります。コルーチンは通常、引数の受け渡しと戻り値によってデータ共有を行い、異なるコルーチン間のデータ分離性は良好です。

  • デバッグとエラー処理: スレッドは通常、デバッグとエラー処理がより複雑です。複数のスレッド間の相互作用と並行実行がデバッグしにくい問題を引き起こす可能性があるためです。コルーチンは、プログラマが実行フローを明示的に制御するため、デバッグとエラー処理が比較的簡単です。

全体的に、スレッドは並行実行が必要なシナリオに適しています。例えば、マルチコアプロセッサ上で並列性を利用してタスクの実行速度を向上させる場合です。一方、コルーチンは協調と調整が必要なシナリオに適しています。例えば、ステートマシン、イベント駆動プログラミング、協調的タスク処理などです。スレッドとコルーチンのどちらを選択するかは、具体的なアプリケーションの要件とプログラミングモデルに依存します。

その他の拡張