Zenではasync
キーワードを使用して関数やメソッドを呼び出すことで、関数やメソッドの処理を途中で中断させたり再開させたりをすることができます。
通常の関数呼び出しでは呼び出した関数の処理が終了するまでは呼び出し元の次の処理を実行することができませんが、async
による呼び出しを行なうことで、ファイルI/Oなどの待ちが発生する処理を途中で中断させて呼び出し元に戻して他の処理を実行し、その後、中断していた箇所から処理を再開させるというような制御ができます。
後ほど説明しますが、async
を使用して呼び出した関数の中断と再開には suspend
, resume
というキーワードを使用します。また、await
キーワードを使用することでasync
を使用して呼び出した関数の完了待ちを行ったり、戻り値を取得したりすることができます。
通常の関数呼び出しとasyncを使用した呼び出しのシーケンスは下図のようになります。
通常の関数呼び出しの場合は関数を呼び出すとその関数から return するまでは呼び出し元の処理は進めることができませんが、async による呼び出しの場合は、関数内で suspend を行なうことで処理を中断し、呼び出し元の処理を継続することができます。また、呼び出し元で resume を行なうことで関数内の中断していた位置から処理を再開することができます。
複数の関数呼び出しを行なう場合は下図のようになります。
ここで、関数A、関数Bではそれぞれ、ファイルリード、ネットワークからのダウンロードという待ちの発生する処理を実行すると想定しましょう。
通常呼び出しの場合はファイルリードを行い、それが完了するのを待ってからネットワークからのダウンロードを開始するというような処理の流れになります。
async による呼び出しの場合は関数Aでファイルリードを開始し、それを待つ間 suspend をして処理を中断し、関数Bを呼び出してネットワークからのダウンロードを開始することができます。更に、ネットワークからのダウンロードを待つ間 suspend をして処理を中断し、ファイルリードが完了した関数Aの処理を再開するということができます。
async
によって呼び出された関数には新たにスタックフレームが割り当てられます。
フレームのハンドルは async
呼び出しの戻り値や、async
で呼び出された関数内で @frame
を呼び出すことで取得できます。
このフレームのハンドルは以降で説明する resume
や await
で使用します。
2つの関数を async
を使用して呼び出した場合のフレームの配置イメージは下図のようになります。
この図のように async
を使用して関数を呼び出した場合はフレームは呼び出し元のスタックに配置されます。ただし、async
ではなく @call
を使用して関数を呼び出した場合のフレームは stack
引数で指定した任意のメモリ領域となります。
次のコードのように async
を使用して呼び出した関数内で suspend
を記述すると、その時点で処理を中断して呼び出し元の処理が再開します。
ここでは、resume
を呼び出していないため suspend
の次の a.* += 1
が実行されることはありません。
examples/ch15-async/async_fn/src/async.zen:4:14
fn suspendFunc(a: *mut u32) void {
a.* += 1;
suspend;
a.* += 1;
}
test "suspend" {
var val: u32 = 1;
_ = async suspendFunc(&mut val);
ok(val == 2);
}
async
を使用して関数を呼び出した場合、戻り値にはその関数のフレームポインタが返されます。
次のコードのように async
経由で呼び出した関数のフレームポインタに対して resume
を記述することで suspend
で中断している処理を再開させることができます。その結果、suspend
の次の a.* += 1
が実行されます。
examples/ch15-async/async_fn/src/async.zen:16:22
test "suspend resume" {
var val: u32 = 1;
var frame = async suspendFunc(&mut val);
ok(val == 2);
resume frame;
ok(val == 3);
}
次のコードのように suspend
と resume
を繰り返し行なうこともできます。
examples/ch15-async/async_fn/src/async.zen:24:41
fn loopSuspendFunc(a: *mut u32) void {
while (true) {
a.* += 1;
suspend;
}
}
test "multiple suspend resume" {
var val: u32 = 1;
var frame = async loopSuspendFunc(&mut val);
ok(val == 2);
resume frame;
ok(val == 3);
resume frame;
ok(val == 4);
resume frame;
ok(val == 5);
}
次のコードのように async
を使用して呼び出した関数のフレームポインタに対して await
を行なうことで async
実行をした関数の戻り値を取得することができます。関数の実行が終了していない場合は終了するまで await
の位置で中断します。
examples/ch15-async/async_fn/src/async.zen:43:66
fn suspendFunc2(a: *mut u32) u32 {
a.* += 1;
suspend;
a.* += 1;
return a.* + 1;
}
fn asyncAwait() void {
var val: u32 = 1;
var frame = async suspendFunc2(&mut val);
ok(val == 2);
resume frame;
ok(val == 3);
const result = await frame;
ok(result == 4);
}
test "async await" {
// test ブロックは async 実行ではないため実質的に中断を行なう await を記述することができない。
// そのため別途関数を用意して async 実行する。
_ = async asyncAwait();
}
次のように async
実行をした関数内で suspend
によって中断している状態で await
する場合は別の非同期関数から resume
されて関数の実行が終了するまで await
の位置で中断したままになります。
fn suspendOneSecond() bool {
suspend {
var frame = @frame();
// 1秒後に誰かにこの frame を resume するように依頼します。
wakeupMeOneSecondLater(frame);
}
return true;
}
fn callAsync() void {
// 1つ目の suspend で制御が戻ります
const frame = async suspendOneSecond();
// 誰かが suspendTwoTimes の frame を resume するまで suspend します
const result = await frame;
}
ノート: suspend している関数を resume する役割は、多くの場合、イベントループが担うことになるでしょう。
suspend
を行っている関数内で @frame
を実行することでその関数のフレームポインタを取得することもできます。次のコードでは @frame
から取得したフレームポインタを使用して resume
を行っています。
examples/ch15-async/async_fn/src/async.zen:68:83
var g_frame: anyframe = undefined;
fn suspendFunc3(a: *mut u32) void {
a.* += 1;
suspend {
g_frame = @frame();
}
a.* += 1;
}
test "at frame" {
var val: u32 = 1;
_ = async suspendFunc3(&mut val);
ok(val == 2);
resume g_frame;
ok(val == 3);
}
既にこれまでのサンプルコードで使用していますが suspend はブロックを持つことができます。
suspend ブロックはブロックの最後まで実行して中断しますが、ブロックの実行を始めた時点で resume 可能となります。この関数が resume されると、ブロックの次から実行再開します。
ノート: suspend ブロック内で更に suspend や suspend ブロックを記述することはできません。
次のコードは標準ライブラリのイベントループのメソッドの一部を抜粋したものです。説明のためにコメントを追記しています。
pub fn yield(self: *mut Loop) void {
suspend {
// この時点から resume 可能
var my_tick_node = NextTickNode{
.prev = undefined,
.next = undefined,
.data = @frame(),
};
self.onNextTick(&mut my_tick_node); // 他の中断処理の再開とその後の resume を依頼
// この時点で中断
}
// resume されるとここから再開
}
ここでは自身の処理を suspend ブロックで中断して、イベントループに対して他の中断している非同期処理の再開とその後に再び自身を再開してもらうことを依頼しています。
イベントループの処理自体は別のコンテキストの非同期処理として動作している場合があるため、例えば、onNextTick
を呼び出した時点で他に中断している処理が存在しないときは suspend ブロックを最後まで実行する前に resume
される可能性があります。
suspend ブロックはブロックの実行を始めた時点で resume
可能となるためそのような場合も問題なく動作することができます。
仮に次のように suspend ブロックを使用せずに実装した場合は suspend
の前に resume
される可能性があり、その際は正常に動作しなくなってしまいます。
pub fn yield(self: *mut Loop) void {
var my_tick_node = NextTickNode{
.prev = undefined,
.next = undefined,
.data = @frame(),
};
self.onNextTick(&mut my_tick_node); // <- この実行中に resume される可能性がある
suspend;
}
☰ 人の生きた証は永遠に残るよう ☰
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.