オフィスアワーがそろそろ始まるよ!()

event

Zen の標準ライブラリでは、非同期処理をサポートするモジュールを std.event で提供しています。 std.event には次のモジュールがあります。

名称 説明
Loop fs の I/O イベント処理、Channel, Future, Lock, RwLock の同期処理の基盤
Channel 多対多の producer/comsumer 間バッファ通信
Future 非同期処理による実行結果の格納
Batch 非同期処理の並列実行
Lock 非同期処理のロック
RwLock 非同期処理の Read/Write ロック

これらのモジュールは async 機能を使用して実装しています。async の詳細ついては、15章 Asyncを参照して下さい。

イベントループの有効化

std.event のモジュールは内部で Loop 構造体のイベントループに依存しており、それらのモジュールを使用するためには標準ライブラリ内に Loop インスタンスを用意する必要があります。また、Batch のパラメータを async_behavior = .auto_async に指定した場合には、イベントループが有効化されている場合に、async 動作になり、そうでない場合は noasync 動作となります。

標準ライブラリ内に Loop インスタンスを用意する方法は次の3種類あります。

root ソースで io_mode を指定

root ソース (通常は main 関数を定義しているソース) に次の行を追記します。

pub const io_mode = .evented;

これにより、標準ライブラリ内のデフォルトの Loop インスタンスが有効化されます。

root ソースで Loop 構造体インスタンス event_loop を宣言

root ソースに次の行を追記します。

examples/ch09-std/event/src/main.zen:4:5

var event_loop_instance = std.event.Loop{};
pub const event_loop = &mut event_loop_instance;

これにより、標準ライブラリ内では Loop インスタンスとして root.event_loop が使用されるようになります。 ただし、この場合は、Loop の処理が自動的には実行されないため次のコードのように、明示的に initrun を呼び出す必要があります。

examples/ch09-std/event/src/main.zen:7:18

pub fn main() anyerror!void {
    try event_loop.init();
    defer event_loop.deinit();

    var frame = async func();

    event_loop.run();
}

fn func() void {
    // 任意の実装
}

ユニットテスト実行時

ユニットテストを実行する際は --test-evented-io オプションを指定することでイベントループが有効になります。

Loop

Loop 構造体は、標準ライブラリ内で fs の I/O イベント処理や Channel, Future, Lock, RwLock の同期処理に使用されます。

fsopen, close, write, read などの I/O の絡む処理は通常は呼び出し元のコンテキストで実行されるので I/O が完了するまで他の処理が実行できません。 イベントループが有効化されている状態ではそれらの呼び出しはイベント要求として Loop のキューに格納され Loop 内のイベントループにて実行されるようになります。 I/O 処理が完了するまでは fs の呼び出し元は suspend により中断され、その間、他の処理が実行できるようになります。

この動作は次のコードで確認できます。

examples/ch09-std/event/src/fs.zen:5:23

test "evented io fs test" {
    warn("\n", .{});
    warn("1: BEFORE writeFile\n", .{});

    var frame = async writeFile();

    warn("4: AFTER  writeFile\n", .{});
    try await frame;
}

fn writeFile() anyerror!void {
    warn("2: IN     writeFile\n", .{});

    const file_name = "test_file.txt";
    try fs.cwd().writeFile(file_name, "Hello, world!\n");
    defer fs.cwd().deleteFile(file_name) catch {};

    warn("3: OUT    writeFile\n", .{});
}

オプションを指定せずにテストを実行すると次の結果のように writeFile 関数の処理が完了してから呼び出し元の処理が実行されます。

$ zen test src/fs.zen 
1/1 test "evented io fs test"...
1: BEFORE writeFile
2: IN     writeFile
3: OUT    writeFile
4: AFTER  writeFile
OK

--test-evented-io オプションを指定してイベントループを有効化すると、次の結果のように writeFile の処理が完了する前に呼び出し元の処理が実行されます。

$ zen test src/fs.zen --test-evented-io
1/1 test "evented io fs test"...
1: BEFORE writeFile
2: IN     writeFile
4: AFTER  writeFile
3: OUT    writeFile
OK

async 実行している関数やメソッドから Loop の関数を直接呼び出すことで自身の処理を中断して他の関数の処理を実行させることができます。 その後、他の関数の処理が中断した際に自動的に中断していた処理を再開してもらうことができます。

この動作をテストコードを使用して説明します。 以下のサンプルコードを実行する場合はイベントループを有効化するため、次のようにして --test-event-io オプションを付けて実行する必要があります。

zen test src/loop.zen --test-evented-io

まずは、Loop を使用しない場合の例です。 次のコードでは fillLogWithoutLoop は引数で渡されたストリームバッファ log に char を格納していくということを5回繰り返しています。

examples/ch09-std/event/src/loop.zen:8:40

test "not using Loop" {
    var buffer = [_]u8{0} ** 20;
    var log = MemOutStream{ .buffer = &mut buffer };
    const expect = "aaaaabbbbbcccccddddd";
    var frame1 = async fillLogWithoutLoop(&mut log, 'a');
    var frame2 = async fillLogWithoutLoop(&mut log, 'b');
    var frame3 = async fillLogWithoutLoop(&mut log, 'c');
    var frame4 = async fillLogWithoutLoop(&mut log, 'd');

    warn("\n", .{});

    // 1, 2, 3, 4 の順で再開させる
    resume frame1;
    resume frame2;
    resume frame3;
    resume frame4;

    await frame1;
    await frame2;
    await frame3;
    await frame4;

    warn("{}\n", .{buffer});
    testing.equalSlices(u8, expect, &mut buffer);
}

fn fillLogWithoutLoop(log: *mut MemOutStream, char: u8) void {
    suspend;
    var i: usize = 0;
    while (i < 5) : (i += 1) {
        _ = log.write(@to(*[1]u8, &char)) catch unreachable;
    }
}

このテストを実行すると fillLogWithoutLoop ではループが完了するまで処理が中断されず log に格納される char の順番が一定となるため結果は常に次のようになります。

1/2 test "Loop not used"...
aaaaabbbbbcccccddddd
OK

次のコードでは fillLogWithLoop のループの中で Loop.startCpuBoundOperation を呼び出すようにしています。startCpuBoundOperation は自身のフレームポインタを Loop のイベントキューに追加して suspend を行います。これにより他の中断している関数に処理を移すことができ、結果として並列で処理が実行されるようになります。 log に格納される内容が予測できないため equalSlices は削除しています。

examples/ch09-std/event/src/loop.zen:42:73

test "using Loop" {
    var buffer = [_]u8{0} ** 20;
    var log = MemOutStream{ .buffer = &mut buffer };
    var frame1 = async fillLogWithLoop(&mut log, 'a');
    var frame2 = async fillLogWithLoop(&mut log, 'b');
    var frame3 = async fillLogWithLoop(&mut log, 'c');
    var frame4 = async fillLogWithLoop(&mut log, 'd');

    warn("\n", .{});

    // 1, 2, 3, 4 の順で再開させる
    resume frame1;
    resume frame2;
    resume frame3;
    resume frame4;

    await frame1;
    await frame2;
    await frame3;
    await frame4;

    warn("{}\n", .{buffer});
}

fn fillLogWithLoop(log: *mut MemOutStream, char: u8) void {
    suspend;
    var i: usize = 0;
    while (i < 5) : (i += 1) {
        _ = log.write(@to(*[1]u8, &char)) catch unreachable;
        Loop.startCpuBoundOperation();
    }
}

このテストを実行すると次のように log 格納される char の順番がバラバラになります。結果は実行するたびに異なります。

2/2 test "Loop.startCpuBoundOperation"...
abcdbcdbcdbcdbcdaaaa
OK

Channel

Channel 構造体は、複数の Producer と複数の Consumer の間でバッファを使用してデータをやり取りする機能を提供します。

バッファが空の状態でデータを取得しようとした Consumer は suspend され、その後、データを格納した Producer によって resume されます。 バッファに空きがない状態でデータを格納しようとした Producer は suspend され、その後、データを取得した Consumer によって resume されます。

Channel は次のメソッドを提供します。

  • init(buffer: []mut T) void: バッファを渡して Channel の初期化を行います。
  • deinit() void: Channel の終了処理を行います。
  • put(data: T) void: バッファへデータを格納します。バッファに空きがない場合は空きができるまで suspend します。
  • get() callconv(.Async) T: バッファに格納されているデータを取得します。バッファが空の場合はデータが格納されるまで suspend します。
  • getOrNull() ?T: バッファに格納されているデータを取得します。バッファが空の場合は null を返します。

次のコードは Channel の各メソッドの使用例です。このテストを実行する場合はイベントループを有効化するため、次のようにして --test-event-io オプションを指定する必要があります。

zen test src/channel.zen --test-evented-io

次のコードでは要素数2のバッファで初期化した Channel を使用して { 1, 2, 3, 4, 5 } の5要素をのやり取りをしています。

examples/ch09-std/event/src/channel.zen:6:39

test "Channel - basic" {
    var buf: [2]i32 = undefined;
    var channel: Channel(i32) = undefined;

    channel.init(&mut buf);
    defer channel.deinit();

    warn("\n", .{});

    var getter = async channelGetter(&mut channel);
    var putter = async channelPutter(&mut channel);

    await getter;
    await putter;
}

fn channelGetter(channel: *mut Channel(i32)) void {
    const expect = [_]i32{ 1, 2, 3, 4, 5 };
    var val: i32 = undefined;

    for (expect) |expect_val| {
        val = channel.get();
        ok(val == expect_val);
        warn("  get {}\n", .{val});
    }
}

fn channelPutter(channel: *mut Channel(i32)) void {
    const put_val = [_]i32{ 1, 2, 3, 4, 5 };
    for (put_val) |val| {
        channel.put(val);
        warn("{} put\n", .{val});
    }
}

このコードを実行した結果は次のようになります。(途中の順番は前後する場合があります。)
Getter を先に実行していますが、バッファが空のため待たされ、Putter によってデータ格納後再開してデータを取得できていること、その後バッファがいっぱいになり Getter がデータを取得するまで Putter が待たされていることなどがわかります。

1/3 test "Channel - basic"...
1 put
2 put
  get 1
3 put
  get 2
4 put
  get 3
5 put
  get 4
  get 5
OK

次のコードでは getOrNull を使用してバッファが空である場合に null が返されることとput によってデータが格納されるとデータを取得できることがわかります。

examples/ch09-std/event/src/channel.zen:41:67

test "Channel - get or null" {
    var buf: [2]i32 = undefined;
    var channel: Channel(i32) = undefined;

    channel.init(&mut buf);
    defer channel.deinit();

    var frame = async channelGetOrNull(&mut channel);

    await frame;
}

fn channelGetOrNull(channel: *mut Channel(i32)) void {
    // buffer is empty
    var valornull = channel.getOrNull();
    ok(valornull == null);

    channelPut(channel, 4);

    // buffer is not empty
    valornull = channel.getOrNull();
    ok(valornull.? == 4);
}

fn channelPut(channel: *mut Channel(i32), val: i32) void {
    channel.put(val);
}

次のコードでは2つの Producer と2つの Consumer の間で1つの Channel を使用してデータのやり取りをしています。

examples/ch09-std/event/src/channel.zen:69:103

test "Channel - multi producer multi consumer" {
    var buf: [2]i32 = undefined;
    var channel: Channel(i32) = undefined;

    channel.init(&mut buf);
    defer channel.deinit();

    warn("\n", .{});

    var getter1 = async channelGetters(&mut channel, 'A');
    var getter2 = async channelGetters(&mut channel, 'B');
    var putter1 = async channelPutters(&mut channel, 'X');
    var putter2 = async channelPutters(&mut channel, 'Y');

    await getter1;
    await getter2;
    await putter1;
    await putter2;
}

fn channelGetters(channel: *mut Channel(i32), id: u8) void {
    var i: usize = 0;
    while (i < 10) : (i += 1) {
        const val = channel.get();
        warn("{c}:    get {:2}\n", .{ id, val });
    }
}

fn channelPutters(channel: *mut Channel(i32), id: u8) void {
    const array = [_]i32{ 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };
    for (array) |val| {
        channel.put(val);
        warn("{c}: {:2} put\n", .{ id, val });
    }
}

このコードを実行すると次のような結果が表示されます。実行するたびに順番は変わりますが、複数の Producer/Consumer 間でデータのやり取りができていることがわかります。

3/3 test "Channel - multi producer multi consumer"...
X: 1  put
B:    get 1 
Y: 1  put
X: 2  put
B:    get 2 
Y: 2  put
B:    get 2 
X: 3  put
B:    get 3 
Y: 3  put
B:    get 3 
X: 4  put
B:    get 4 
Y: 4  put
B:    get 4 
X: 5  put
B:    get 5 
Y: 5  put
B:    get 5 
X: 6  put
B:    get 6 
Y: 6  put
A:    get 1 
X: 7  put
Y: 7  put
A:    get 6 
A:    get 7 
X: 8  put
Y: 8  put
A:    get 7 
A:    get 8 
X: 9  put
Y: 9  put
A:    get 8 
A:    get 9 
X: 10 put
Y: 10 put
A:    get 9 
A:    get 10
A:    get 10
OK

Future

Future 構造体は何らかの処理を非同期実行させる際にその処理結果を必要になったタイミングで取得する機能とその処理が完了したかどうかを確認する機能を提供します。

Future 構造体には処理結果を格納するフィールド data があり、インスタンス生成時は data の値は使用不可状態になっています。 data フィールドに処理結果を格納し resolve を呼び出すことで、処理結果が取得可能になります。 resolve 呼び出し前に get を呼び出すと suspend され、resolve が呼び出されると resumedata フィールドへのポインタが返されます。 また、get の代わりに getOrNull を呼び出すことで resolve が呼び出されているかどうかをチェックすることができます。

Future は次のメソッドを提供します。

  • resolve() void: data フィールドの値を有効化します。このメソッドを呼び出す前に data に値を格納しておく必要があります。
  • get() callconv(.Async) *mut T: 格納された値の取得を行います。まだ resolve が呼び出されていない場合は suspend します。
  • getOrNull() ?*mut T: 格納された値の取得を試みます。まだ resolve が呼び出されていない場合は null を返します。
  • start() callconv(.Async) ?*mut T: Future が保持する data を使用して処理を開始することを伝えます。既に開始状態であれば resolve が呼ばれるまで suspend します。

次のコードでは resolve 呼び出し前に get 呼び出すと suspend されることと、resolve が呼び出されると get 呼び出しの処理が再開され Future に格納された値を取得できることを示します。

examples/ch09-std/event/src/future.zen:6:25

test "Future - basic" {
    var future = Future(u32).init();

    var frame1 = async resolver(&mut future);
    var frame2 = async getter(&mut future);
    resume frame1;

    await frame1;
    await frame2;
}

fn getter(future: *mut Future(u32)) void {
    ok(future.get().* == 42);
}

fn resolver(future: *mut Future(u32)) void {
    suspend;
    future.data = 42;
    future.resolve();
}

getOrNull は次のコードのように resolve 呼び出し前では null を返し、resolve 呼び出し後は値が格納された data フィールドへのポインタを返します。これにより、非同期で実行している処理が完了しているかどうかを確認することができます。

examples/ch09-std/event/src/future.zen:27:41

test "Future - getOrNull" {
    var future = Future(u32).init();

    var frame = async resolver(&mut future);

    // future is unavailable
    ok(future.getOrNull() == null);

    resume frame;
    await frame;

    // future is resolved
    var result = future.getOrNull().?;
    ok(result.* == 42);
}

start は初回呼び出し時には null を返します。2回目以降に呼び出すと resolve が呼び出されるまで suspend します。その後、resolve が呼び出されると data フィールドへのポインタを返します。 この動作により、複数の非同期処理で同一の Future に対して処理を実行することを防げます。

examples/ch09-std/event/src/future.zen:43:72

test "Future - start" {
    var future = Future(u32).init();

    var frame1 = async resolverWithStart(&mut future);
    var frame2 = async resolverWithStart(&mut future);

    const result1 = await frame1;
    const result2 = await frame2;

    // not started
    ok(result1 == false);

    // already started and resolved
    ok(result2 == true);

    ok(future.get().* == 42);
}

fn resolverWithStart(future: *mut Future(u32)) bool {
    var frame = async future.start();
    const started = await frame;
    if (started) |val| {
        ok(val.* == 42);
        return true;
    } else {
        future.data = 42;
        future.resolve();
        return false;
    }
}

Batch

Batch は複数の async 関数(ジョブ)を並列で実行する機能を提供します。

Batch は次の3つのパラメータを指定することで構造体型を返します。

  • comptime Result: type: ジョブの戻り値型を指定します。
  • comptime max_jobs: comptime_int: 並列実行する最大ジョブ数です。
  • comptime async_behavior: enum: add, wait メソッドを async とするかどうかを指定します。

async_behavior に指定できる値は次の3種類です。

  • auto_async: イベントループが有効かどうかによって決定します。
  • never_async: async 関数になりません。
  • always_async: async 関数となります。

Batch は次のメソッドを提供します。

  • add(frame: anyframe->Result) void: ジョブを追加します。
  • wait() CollectedResult: 全てのジョブが完了するのを待ちます。

Batchaddwait の基本的な使用例を説明します。add メソッドでジョブとして async 実行した関数のフレームを追加し、wait メソッドでそれらのジョブが全て完了するのを待っています。実行するジョブは Loop の例で使用したのとほぼ同じもので、引数で渡されたストリームバッファに char を5回格納する関数です。

次のコードでは async_behavior.auto_async を指定しています。.auto_async を指定している場合はイベントループが有効になっているかどうかで挙動が変わります。

examples/ch09-std/event/src/batch.zen:9:33

test "Batch - auto_async" {
    var batch = Batch(void, 4, .auto_async).init();
    var buffer = [_]u8{0} ** 20;
    var log = MemOutStream{ .buffer = &mut buffer };
    const expect = "aaaaabbbbbcccccddddd";

    batch.add(&async fillLogWithLoop(&mut log, 'a'));
    batch.add(&async fillLogWithLoop(&mut log, 'b'));
    batch.add(&async fillLogWithLoop(&mut log, 'c'));
    batch.add(&async fillLogWithLoop(&mut log, 'd'));
    batch.wait();

    warn("\n{}\n", .{buffer});
    if (!std.io.is_async) {
        testing.equalSlices(u8, expect, &mut buffer);
    }
}

fn fillLogWithLoop(log: *mut MemOutStream, char: u8) void {
    var i: usize = 0;
    while (i < 5) : (i += 1) {
        _ = log.write(@to(*[1]u8, &char)) catch unreachable;
        Loop.startCpuBoundOperation();
    }
}

まず、イベントループを有効にせずに実行してみます。すると、常に呼び出した順に5要素ずつ char が格納されるようになります。これは、イベントループが有効になっていない場合 startCpuBoundOperation が何も行っておらず、fillLogWithLoop が中断することがないためです。

$ zen test src/batch.zen 
1/1 test "Batch - auto_async"...
aaaaabbbbbcccccddddd
OK

次に、イベントループを有効にして実行してみます。すると、char の格納される順番がバラバラになります。これは Loop の場合と同様に startCpuBoundOperation によって処理を中断させてイベントループに回させているからです。この場合、テスト結果が NG になるのでイベントループが有効の場合に true となる std.io.is_async で判定して equalSlices を呼び出さないようにしています。

$ zen test src/batch.zen --test-evented-io
1/1 test "Batch - auto_async"...
abcdbcdbcdbcdbcdaaaa
OK

このようにして、イベントループと Batch を組み合わせることによって例えば I/O 処理など待ち時間のある処理を並列に実行させ、それらの完了を1箇所で待ち合わせるような使い方ができます。async_behavior.auto_async をしておくとイベントループが有効な場合でも無効な場合でもコードを書き換えることなく対応することができます。イベントループの有無によらず Batchadd, wait の挙動を固定したい場合は必要に応じで async_behavior.never_async.always_async を指定することで制御できます。

Lock

Lock 構造体は非同期処理の排他制御機能を提供します。Lock を使用して1度に取得できるロックは1つだけです。

ロックの取得は Lock 構造体を使用して行い、取得したロックの開放処理は Lock.Held 構造体によって行います。

Lock は次の関数とメソッドを提供します。

  • init() Lock: Lock インスタンスを生成します。ロックしていない状態で初期化します。
  • initLocked() Lock: Lock インスタンスを生成します。ロックしている状態で初期化します。
  • acquire() callconv(.Async) Held: ロックを取得し Lock.Held インスタンスを返します。既にロックされている場合はロックを取得できるまで suspend します。

取得したロックの開放は Lock.Held インスタンスによって行います。

Lock.Held は次のメソッドを提供します。

  • release() void: ロックを開放します。ロック取得待ちで suspend している処理があれば resume させます。

次のコードで Lock の使用例を説明します。 Loop の例で使用した fillLogWithLoop を変更して log に char を格納するループを Lock によって排他制御するようにしています。これによって処理の実行順を制御できるようになり log に格納される char の順番が一定になります。

examples/ch09-std/event/src/lock.zen:9:48

test "Lock - basic" {
    var buffer = [_]u8{0} ** 20;
    var log = MemOutStream{ .buffer = &mut buffer };
    var lock = Lock.init();
    const expect = "aaaaabbbbbcccccddddd";
    var frame1 = async fillLogWithLock(&mut lock, &mut log, 'a');
    var frame2 = async fillLogWithLock(&mut lock, &mut log, 'b');
    var frame3 = async fillLogWithLock(&mut lock, &mut log, 'c');
    var frame4 = async fillLogWithLock(&mut lock, &mut log, 'd');

    warn("\n", .{});

    // 1, 2, 3, 4 の順で再開させる
    resume frame1;
    resume frame2;
    resume frame3;
    resume frame4;

    await frame1;
    await frame2;
    await frame3;
    await frame4;

    warn("{}\n", .{buffer});
    testing.equalSlices(u8, expect, &mut buffer);
}

fn fillLogWithLock(lock: *mut Lock, log: *mut MemOutStream, char: u8) void {
    suspend;

    var lock_frame = async lock.acquire();
    const lock_handle = await lock_frame;
    defer lock_handle.release();

    var i: usize = 0;
    while (i < 5) : (i += 1) {
        _ = log.write(@to(*[1]u8, &char)) catch unreachable;
        Loop.startCpuBoundOperation();
    }
}

このテストを実行すると常に次のような結果になります。

$ zen test src/lock.zen --test-evented-io
1/1 test "Lock - basic"...
aaaaabbbbbcccccddddd
OK

RwLock

RwLock 構造体は非同期処理の排他制御機能を提供します。 読み込み用のロック(リード・ロック)は同時に複数取得することができますが、書き込み用のロック(ライト・ロック)は排他されます。

ロックの取得は RwLock 構造体を使用して行い、リード・ロックの開放処理は RwLock.HeldRead 構造体、ライト・ロックの開放処理は RwLock.HeldWrite 構造体によって行います。

RwLock は次の関数とメソッドを提供します。

  • init() RwLock: RwLock インスタンスを生成します。
  • acquireRead() callconv(.Async) HeldRead: リード・ロックを取得し RwLock.HeldRead インスタンスを返します。既にライト・ロックが取得されている場合は開放されるまで suspend します。
  • acquireWrite() callconv(.Async) HeldWrite: ライト・ロックを取得し RwLock.HeldWrite インスタンスを返します。既にロックされている場合はロックを取得できるまで suspend します。

RwLock.HeldRead は次のメソッドを提供します。

  • release() void: リード・ロックを開放します。リード・ロックが全て開放された際に、ライト・ロック取得待ちで suspend している処理があれば resume させます。

RwLock.HeldWrite は次のメソッドを提供します。

  • release() void: ライト・ロックを開放します。ロック取得待ちで suspend している処理があれば resume させます。

サンプルコードを示しながら RwLock の動作を説明します。

RwLock の動作を説明するために次の4種類の関数を使用します。それぞれ、リード・ロックを取得してロックを開放するだけの関数、リード・ロック後に suspend する関数、同様にライト・ロックを取得するだけの関数、ライト・ロック取得後に suspend する関数です。

examples/ch09-std/event/src/rwlock.zen:7:51

fn readLocker(lock: *mut RwLock, id: u8) void {
    var frame = async lock.acquireRead();
    const handle = await frame;
    defer {
        handle.release();
        warn("{c}: release read lock\n", .{id});
    }

    warn("{c}: acquired read lock\n", .{id});
}

fn readLockerWithSuspend(lock: *mut RwLock, id: u8) void {
    var frame = async lock.acquireRead();
    const handle = await frame;
    defer {
        handle.release();
        warn("{c}: release read lock\n", .{id});
    }

    warn("{c}: acquired read lock\n", .{id});
    suspend;
}

fn writeLocker(lock: *mut RwLock, id: u8) void {
    var frame = async lock.acquireWrite();
    const handle = await frame;
    defer {
        handle.release();
        warn("{c}: release write lock\n", .{id});
    }

    warn("{c}: acquire write lock\n", .{id});
}

fn writeLockerWithSuspend(lock: *mut RwLock, id: u8) void {
    var frame = async lock.acquireWrite();
    const handle = await frame;
    defer {
        handle.release();
        warn("{c}: release write lock\n", .{id});
    }

    warn("{c}: acquire write lock\n", .{id});
    suspend;
}

次のコードでリード・ロック中にリード・ロック、ライト・ロックを取得しようとした際の動作を説明します。 readLockerWithSuspendasync 実行してリード・ロックした状態で readLocker, writeLocker を呼び出しています。

examples/ch09-std/event/src/rwlock.zen:53:67

test "RwLock - read lock" {
    var lock = RwLock.init();
    defer lock.deinit();

    warn("\n", .{});
    var reader1 = async readLockerWithSuspend(&mut lock, 'a');
    var reader2 = async readLocker(&mut lock, 'b');
    var writer1 = async writeLocker(&mut lock, 'c');

    resume reader1;

    await reader1;
    await reader2;
    await writer1;
}

このテストを実行すると次のような結果が表示されます。 id:'a' の readLockerWithSuspend がリード・ロックを取得している間に、id:'b' の readLocker でリード・ロックが取得できていること、writeLocker では id:'a', 'b' のリード・ロックが両方とも開放されるまでライト・ロックが取得できていないことがわかります。

1/2 test "RwLock - read lock"...
a: acquired read lock
b: acquired read lock
b: release read lock
a: release read lock
c: acquire write lock
c: release write lock
OK

次のコードでライト・ロック中にライト、リード・ロックを取得しようとした際の動作を説明します。 writeLockerWithSuspendasync 実行してライト・ロックした状態で writeLocker, readLocker を呼び出しています。

examples/ch09-std/event/src/rwlock.zen:69:83

test "RwLock - write lock" {
    var lock = RwLock.init();
    defer lock.deinit();

    warn("\n", .{});
    var writer1 = async writeLockerWithSuspend(&mut lock, 'a');
    var writer2 = async writeLocker(&mut lock, 'b');
    var reader1 = async readLocker(&mut lock, 'c');

    resume writer1;

    await writer1;
    await writer2;
    await reader1;
}

このテストを実行すると次のような結果が表示されます。 id:'a' の writeLockerWithSuspend がライト・ロックを取得している間は、id:'b' の writeLocker がライト・ロックを取得できないこと、ライト・ロックが開放されるまで id:'c' の readLocker がリード・ロックを取得できないことがわかります。

2/2 test "RwLock - write lock"...
a: acquire write lock
a: release write lock
b: acquire write lock
b: release write lock
c: acquired read lock
c: release read lock
OK

Chapter 1

Chapter 2

Chapter 3

Chapter 4

Chapter 5

Chapter 6

Chapter 7

Chapter 8

Chapter 9

Chapter 10

Chapter 11

Chapter 12

Chapter 13

Chapter 14

Chapter 15

Appendix

Error Explanation

☰ 人の生きた証は永遠に残るよう ☰
Copyright © 2018-2020 connectFree Corporation. All rights reserved. | 特定商取引法に基づく表示
Zen, the Zen three-circles logo and The Zen Programming Language are trademarks of connectFree corporation in Japan and other countries.