LemonHX

LemonHX

CEO of Limit-LAB 喜欢鼓捣底层的代码,意图改变世界
twitter
tg_channel

Go言語が2回勝利し、遥かにリードしています

Goroutines は Go 言語において協調的なマルチタスク処理を実現するためのメカニズムであり、Go をシンプルかつ効率的な並行プログラミング言語にしています。

最も重要なのは、このものは初心者に Async とは何かを教えるよりもずっと便利です

皆さん、何もないところで反論しないでください。私は昨日、全く Go の経験がない人に 2 時間で Go を学ばせました

Go では、各 goroutine は独立したスタックを持ち、一時的な値や戻りアドレスなどの情報を保存できます。実際、Go コンパイラは各 goroutine が開始される際に特に小さなスタックを割り当て、必要に応じて拡張します。

ここで、なぜ go と async が本質的に同じことをしているのかを説明します

Go 言語の TinyGo の goroutines の実装を例にとります。これは C#、JavaScript、そして現在は C++ でも使用されている async/await モデルに基づいています。実際、TinyGo は自動的にasync/await キーワードを挿入するようです。なぜなら、TinyGo はシングルスレッドだからです。

以下のコードスニペットを例に見てみましょう:

func main() {
	go background()
	time.Sleep(2 * time.Second)
	println("他の操作")
	n := compute()
	println("完了:", n)
}

func background() {
	for {
		println("バックグラウンド操作...")
		time.Sleep(time.Second)
	}
}

func compute() int {
	time.Sleep(time.Second)
	println("ブロッキング操作が完了しました")
	return 42
}

もし私たちが手動で async await を追加して JS の人がより馴染みのある構文に変えたら

async func main() {
	go background()
	await time.Sleep(2 * time.Second)
	println("他の操作")
	n := await compute()
	println("完了:", n)
}

async func background() {
	for {
		println("バックグラウンド操作...")
		await time.Sleep(time.Second)
	}
}

async func compute() int {
	await time.Sleep(time.Second)
	println("ブロッキング操作が完了しました")
	return 42
}

私はほとんどのユーザーがここまで問題ないと信じていますが、急がないでください。本当に async await を学ぶ必要はありません。

Delimited Continuation を導入します#

理解できない場合は、記事 Delimited Continuation をご覧ください。

Delimited Continuation(限定された継続)は、プログラムの実行フローを制御し、必要に応じて実行状態を保存および復元するためのプログラミングモデルです。これは、開発者のメンタル負担を軽減する柔軟で強力な制御フロー転送メカニズムを提供します。

従来の制御フローでは、関数の実行順序は線形であり、各関数には入口と出口があります。ある関数が別の関数を呼び出すと、制御は呼び出された関数に移り、呼び出された関数が完了すると、制御は呼び出し元の関数に戻ります。この線形制御フローは、非同期プログラミングにおけるコールバック地獄やコルーチンの実装など、いくつかの複雑なプログラミングニーズには適していません。

限定された継続は、関数の任意の位置で現在の実行状態を保存し、制御を別のコードスニペットに移すことを可能にします。このコードスニペットは継続と呼ばれ、クロージャ、関数、または他の実行可能なコードブロックである可能性があります。限定された継続は、実行状態を異なるコードスニペットに持ち運ぶ能力を提供し、非局所的な制御フロー転送を実現します。

限定された継続に関連するのは Async Await モデルであり、これは非同期操作を簡素化するためのプログラミングモデルで、現代の非同期プログラミングで広く使用されています。Async Await は、開発者が明示的にコールバック関数を処理することなく、同期的な方法で非同期コードを書くことを可能にします。これは、非同期操作の完了を待つために関数の実行を一時停止および復元し、非同期操作が完了した後に後続のコードを実行し続けます。

限定された継続と Async Await の間には関連性と共有の概念があります。Async Await は実際には限定された継続に基づいて実装された非同期プログラミングモデルです。Await を使用して非同期関数を一時停止すると、実際には現在の実行状態が限定された継続に保存され、非同期操作が完了した後に実行が復元されます。

限定された継続とコンパイラによる自動変換の利点は、開発者のメンタル負担を大幅に軽減できることです。限定された継続の変換を自動的に処理することで、開発者は複雑な制御フロー操作を手動で処理する必要がなくなり、コードがよりシンプルで可読性が高くなります。この変換の自動化は、開発者が底層の制御フローの詳細ではなく、ビジネスロジックに集中できるようにする高い抽象レベルを提供します。

自動的に限定された継続の変換を処理する利点には以下が含まれます:

  1. 非同期プログラミングの簡素化: 限定された継続を使用することで、非同期プログラミングモデルが簡素化され、非同期コードの記述がより直感的で理解しやすくなります。開発者は、複雑なコールバック関数やネストを処理することなく、同期コードを書くように非同期機能を使用できます。

  2. エラーの削減と保守性の向上: 自動的に限定された継続の変換を処理することで、コーディング中のエラーを減らし、コードの保守性を向上させることができます。この変換はコンパイラによって自動的に行われるため、手動で制御フローの問題を処理する際に一般的なエラーが発生しにくくなります。

  3. 複雑な制御フローの簡素化: 限定された継続は、より柔軟で細かい制御フローの転送を提供し、複雑な制御フローパターンの処理を容易にします。開発者は、限定された継続を使用して、コルーチンや状態機械などのより高レベルの制御構造を実現できます。

皆さん、気づきましたか?#

Go 言語の最大の利点は、全体の言語が線形であり、奇妙な制御フローがないことです(recover しない限り)。言語には唯一の並行手段であるgoがあるため、私たちはそれを delimited continuation 形式に非常に簡単に変換できるのではないでしょうか?

私のコンパイラには、go コードを制御フローに基づいて delimited continuation 形式に変換するパスがあると仮定します。

このパスは非常にシンプルです。なぜなら、AST を CPS 形式の AST に変換するのは、私の師匠が 100 行のコードでできるからです。

私たちは次のようなコードを得るでしょう。

func main() {
    background(func() {
        time.Sleep(2 * time.Second)
        println("他の操作")
        compute(func(n int) {
            println("完了:", n)
        })
    })
}

func background(k func()) {
    for {
        println("バックグラウンド操作...")
        time.Sleep(time.Second)
        k()
    }
}

func compute(k func(int)) {
    time.Sleep(time.Second)
    println("ブロッキング操作が完了しました")
    k(42)
}

そして、次にこの単純な delimited continuation を LLVM のコルーチンに翻訳するためのパスを通します。

C++ の友人に感謝します。彼が特性を積み重ねてくれたおかげで、LLVM には非常に便利な機能があります。

func main(parent *coroutine) {              // 注意: parentは常にnilです。これはmainだからです
  hdl := llvm.makeCoroutine()             // 私のコルーチン
  background(nil)                         // 新しい独立したgoroutineなので親を渡しません
  runtime.sleepTask(hdl, 2 * time.Second) // この関数をスリープとしてマーク
  llvm.suspend(hdl)                       // このコルーチンを一時停止
  println("他の操作")
  compute(hdl)                            // 継続渡しスタイル
  llvm.suspend(hdl)
  n := hdl.data
  println("完了:", n)
  runtime.resumeTask(parent)              // 親を再活性化(不要)
}

func background(parent *coroutine) {
  hdl := llvm.makeCoroutine()
  for {
    println("バックグラウンド操作...")
    runtime.sleepTask(hdl, time.Second) // この関数をスリープとしてマーク
    llvm.suspend(hdl)
  }
// コードは到達不能なので、runtime.resumeTaskの呼び出しはありません。
}

func compute(parent *coroutine) {
  hdl := llvm.makeCoroutine()
  runtime.sleepTask(hdl, time.Second) // この関数をスリープとしてマーク
  llvm.suspend(hdl)
  println("ブロッキング操作が完了しました")
  parent.data = 42                    // 注意: まだ実装されていません
  runtime.resumeTask(parent)          // 親を再活性化
}

そして、最後に background の hdl をあるタスクキューにスパーンさせて、全プロセスが完了します。

なぜ私たちは Go が 2 回勝ったと言うのか?#

CPS への変換の難易度:#

CPS への変換は、プログラム全体を継続関数形式で制御フローを渡すように変換します。Go 言語では、go関数と<-の 2 つの制御フロー操作しかないため、コードを CPS スタイルに変換するのは比較的簡単です。

自動染色の Async Await の難易度:#

自動染色の Async Await を実現するには、コンパイラとランタイム環境の複雑性を考慮する必要があります。この自動化変換は、コンパイラがコードを静的に分析し、変換し、非同期操作を一時停止点としてマークし、適切なタイミングで実行を復元する必要があります。コンパイラは構文解析、意味解析、および対応する中間表現またはターゲットコードを生成する必要があります。

自動染色の Async Await は、技術的な複雑性が高いです。このパスは、複雑な制御フロー、コード生成、および最適化の問題を処理する必要があります。また、コンパイラと底層言語の実行メカニズムについて深く理解する必要があります。

実行時のリソース消費状況#

実行時に、自動染色の Async Await モデルは通常、非同期タスクの状態、停止点のコンテキストなどの追加の状態を維持する必要があります。これらの状態は追加のメモリと処理オーバーヘッドを必要とし、ランタイムの複雑性を増加させる可能性があります。同時に、非同期操作を実行するには、コルーチンやスレッドの実行をスケジュールおよび管理する必要があり、コンテキストスイッチやスケジューラのオーバーヘッドが関与します。

その後、私たちは C++ の友人に感謝します。彼が提供してくれた LLVM のコルーチンは、コードを変換し、状態機械を生成し、コルーチンの切り替えポイントを最適化することで、高効率なコルーチン実行を実現します。これは、コルーチンの切り替えポイントの位置をマークし、生成された状態機械を利用してコルーチンの切り替えを制御します。これにより、不必要なコンテキストスイッチを回避し、パフォーマンスのオーバーヘッドとメモリ使用量を減少させることができます。

まとめ#

Go は手を抜き、技術が進化し、Go は再び勝ったようです。

読み込み中...
文章は、創作者によって署名され、ブロックチェーンに安全に保存されています。