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

Async

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 呼び出しの戻り値や、async で呼び出された関数内で @frame を呼び出すことで取得できます。 このフレームのハンドルは以降で説明する resumeawait で使用します。

2つの関数を async を使用して呼び出した場合のフレームの配置イメージは下図のようになります。

この図のように async を使用して関数を呼び出した場合はフレームは呼び出し元のスタックに配置されます。ただし、async ではなく @call を使用して関数を呼び出した場合のフレームは stack 引数で指定した任意のメモリ領域となります。

async 呼び出しと suspend

次のコードのように 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);
}

suspend からの resume

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 の繰り返し

次のコードのように suspendresume を繰り返し行なうこともできます。

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 を行なうことで 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 する役割は、多くの場合、イベントループが担うことになるでしょう。

@frame による関数フレームポインタの取得

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 はブロックを持つことができます。

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;
}

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.