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 ソース (通常は main 関数を定義しているソース) に次の行を追記します。
pub const io_mode = .evented;
これにより、標準ライブラリ内のデフォルトの 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
の処理が自動的には実行されないため次のコードのように、明示的に init
と run
を呼び出す必要があります。
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
構造体は、標準ライブラリ内で fs
の I/O イベント処理や Channel
, Future
, Lock
, RwLock
の同期処理に使用されます。
fs
の open
, 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
構造体は、複数の 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
構造体には処理結果を格納するフィールド data
があり、インスタンス生成時は data
の値は使用不可状態になっています。
data
フィールドに処理結果を格納し resolve
を呼び出すことで、処理結果が取得可能になります。
resolve
呼び出し前に get
を呼び出すと suspend
され、resolve
が呼び出されると resume
し data
フィールドへのポインタが返されます。
また、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
は複数の 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
: 全てのジョブが完了するのを待ちます。Batch
の add
と wait
の基本的な使用例を説明します。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
をしておくとイベントループが有効な場合でも無効な場合でもコードを書き換えることなく対応することができます。イベントループの有無によらず Batch
の add
, wait
の挙動を固定したい場合は必要に応じで async_behavior
に .never_async
や .always_async
を指定することで制御できます。
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.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;
}
次のコードでリード・ロック中にリード・ロック、ライト・ロックを取得しようとした際の動作を説明します。
readLockerWithSuspend
を async
実行してリード・ロックした状態で 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
次のコードでライト・ロック中にライト、リード・ロックを取得しようとした際の動作を説明します。
writeLockerWithSuspend
を async
実行してライト・ロックした状態で 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
☰ 人の生きた証は永遠に残るよう ☰
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.