7
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

GoAdvent Calendar 2024

Day 2

『Go言語呪文の書』時空魔法goroutineで学ぶ並行処理入門

Last updated at Posted at 2024-12-01

驚くべきことにGo言語では時空魔法が実装されている。この魔法はコードの流れ(フロー)を変えてしまい並行処理と非同期処理を実現してしまう。ここではこの魔法について述べる。

image.png

goroutine 〜並行処理の時空魔法〜

goroutineとは、神託機械(コンピュータ)の中で時空を歪ませ、複数の処理の同時実行を具現化するGo言語固有の魔術体系である。これは通常の関数呼び出しに「go」という呪文を付与することで発動する。発動するたびに時空が歪み、次々に神託機械の一部が切り取られ神託機械の分身(軽量スレッド)が使い魔として召喚される。全体の総計算能力はほぼ変わらないものの使い魔は各々が独立した神託機械として機能する。この魔法は非常に強力で神託機械の記憶装置(メモリ)と計算装置(CPU)をわずかながら消費し一度に多数の使い魔を操れる。しかし、そのことが逆に術者(開発者)の精神力を大きく蝕む。代償は大きいがその価値は十分にある。

基本的な使い方を記す。

従来の処理の流れ

まずはgoroutineを使わずに関数を使う例を示す。この神託機械は火炎魔法と氷魔法を扱えるが、それぞれ2秒の詠唱時間が必要だ。これを使って夕食を作ろう。

package main

import (
        "fmt"
        "time"
)

func useFireSpell() {
    time.Sleep(2 * time.Second)  // 2秒の詠唱時間
    fmt.Println("火炎魔法で肉を焼いた!🔥🔥🔥")
}

func useIceSpell() {
    time.Sleep(2 * time.Second)  // 2秒の詠唱時間
    fmt.Println("氷魔法でワインを冷やした!🧊🧊🧊")
}

func main() {
    fmt.Println("開始")
    start := time.Now()
    useFireSpell()
    useIceSpell()
    fmt.Println("完了")
    elapsed := time.Since(start)
    fmt.Printf("処理時間: %s秒\n", elapsed)
}

実行結果

開始
火炎魔法で肉を焼いた!🔥🔥🔥
氷魔法でワインを冷やした!🧊🧊🧊
完了
処理時間: 4.0018788秒

当然ながら、すべての魔法が完了するには4秒かかる。

goroutine並行処理の魔術

goroutineでは、通常の関数の前にgoという呪文を唱えると時空が歪み、新たな神託機械が出現する。関数は新たな神託機械で実行を開始するはずなのだが...

package main

import (
        "fmt"
        "time"
)

func useFireSpell() {
    time.Sleep(2 * time.Second)  // 2秒の詠唱時間
    fmt.Println("火炎魔法で肉を焼いた!🔥🔥🔥") 
}

func useIceSpell() {
    time.Sleep(2 * time.Second)  // 2秒の詠唱時間
    fmt.Println("氷魔法でワインを冷やした!🧊🧊🧊")
}

func main() {
    fmt.Println("開始")
    start := time.Now()
    go useFireSpell()
    go useIceSpell()
    fmt.Println("完了")
    elapsed := time.Since(start)
    fmt.Printf("処理時間: %s秒\n", elapsed)
}

しかし、このコードは動作しない。火炎魔法も氷魔法も実行されず、すぐに終わってしまう。召喚したuseFireSpell()とuseIceSpell()の終了を待たないからだ。正しく動作するようには次のようにする。

package main

import (
    "fmt"
    "time"
)

func useFireSpell(done chan bool) {
    fmt.Println("火炎魔法の詠唱開始...")
    time.Sleep(2 * time.Second)
    fmt.Println("火炎魔法で肉を焼いた!🔥🔥🔥")
    done <- true
}

func useIceSpell(done chan bool) {
    fmt.Println("氷魔法の詠唱開始...")
    time.Sleep(2 * time.Second)
    fmt.Println("氷魔法でワインを冷やした!🧊🧊🧊")
    done <- true
}

func main() {
    fmt.Println("開始")
    start := time.Now()

    done := make(chan bool)
    
    go useFireSpell(done)
    go useIceSpell(done)

    // 両方の魔法の完了を待つ
    <-done
    <-done

    elapsed := time.Since(start)
    fmt.Println("完了")
    fmt.Printf("処理時間: %s\n", elapsed)
}

今度は成功した。火炎魔法と氷魔法はgoが生み出したそれぞれの使い魔が実行するので2秒で済む。効果は絶大で、あっという間に晩餐が開始できる。
image.png

開始
火炎魔法の詠唱開始...
氷魔法の詠唱開始...
氷魔法でワインを冷やした!🧊🧊🧊
火炎魔法で肉を焼いた!🔥🔥🔥
完了
処理時間: 2.0001591秒

改めて起きたことを説明する。重要な部分を書き出す。

main関数
done := make(chan bool) で魔法通信路を開く
go useFireSpell(done) で火炎魔法を詠唱する使い魔を召喚する
go useIceSpell(done) で氷魔法を詠唱する使い魔を召喚する
<-done 1つ目の完了を待つ(useFireSpell/useIceSpell内部で、 done <- trueが実行するのを待つ)
<-done 2つ目の完了を待つ(useFireSpell/useIceSpell内部で、 done <- trueを実行するのを待つ)
useFireSpell/useIceSpell関数
done <- true

動作しないの例でも魔法を発動しようとしている。しかし、魔法が発動される前に使い魔を召喚した神託機械の本体が全体を終了しているのだ。使い魔の魔法の完了を本体が検知できていないのだ。そこで登場するのが「魔法通信路(チャネル)」である。まず魔法通信路を開く(done := make(chan bool))。これは神託機械の本体が使い魔たちと交信するための特別な経路だ。この経路は魔法の世界では「門」のように機能する。印を送る者(done <- true)は門の前で立ち止まり、受け取る者(<-done)が現れるまで待つ。同様に、受け取る者も、送る者が現れるまでその場で待ち続ける。この門は単なる通路ではない。それは魔法使いと使い魔を結ぶ絆そのものだ。送る側と受け取る側が出会うまで、両者は確実に待ち合わせる。この魔法の門によって、使い魔たちは確実に本体と交信できるのだ。今回は魔法の成否(true/false)を伝えるだけの単純な門だが、より複雑な情報を伝える門を開くこともできる。

神託機械とその分身である使い魔は互いの存在を感じ取っているのだ。

神託機械と使い魔

古の大賢者チューリングは、すべての要求を予言できる究極の神託機械の設計図を残した。現代の神託機械は、その叡智を受け継ぐ存在なのだ。大賢者の設計図をもとに賢明なエンジニアたちが作った神託機械(コンピュータ)は人々の要求を次々と実現した。しかし、人の要求はとどまることを知らなかった。そこでさらに多くの要求を実現するためにGo言語のエンジニアたちは時空を歪ませ神託機械の分身(軽量スレッド)を使い魔として作ることを思いついた。時空を歪ませる技術の実現には困難を極めたが高いレベルで完成した。人々は無限の要求が実現すると歓喜したのだった....

だが、そんなことはありえないことをGo言語のエンジニアたちは知っていた。神託機械の計算能力(CPU)と記憶能力(メモリ)には限りがあるのだ。無限の要求が実現することはない。しかし神託機械(コンピュータ)には、もう一つの不思議な性質があった。それは神託を与える際の「瞑想の時間」である。神託機械は人々の要求に応えるとき、しばしば長い瞑想(待機)に入る。それは遠い世界から知恵を集めるため(ディスクI/O)であったり、他の神託機械と対話するため(ネットワークI/O)であったり、時には人々からの返答を待つため(ユーザー入力)であった。呪文を実行するための詠唱もその一つである。

神託機械が瞑想している間、その計算能力(CPU)は眠りについているようなものだと賢明なGo言語のエンジニアたちは気づいていたのだ。そこで彼らは、ある使い魔が瞑想している間に、別の使い魔が働けるようにした。これにより神託機械は休むことなく、人々の要求に応え続けた。

Go言語を作り出したエンジニアたちは、さらなる叡智を重ね、神託機械の核心部である言語そのものに分身術を織り込んだ。これにより神託機械の基底部分(OS)に頼ることなく、より少ないコストで分身を生み出すことを可能とし絶大な力を手にいれた。

まさに神託機械は、goroutineによって待つことを知恵に変えた。
image.png

次なる探求

時空魔法、goroutineの並行処理と非同期処理について基本的な使い方を我々は手にした。しかし、これはまだ入り口に過ぎない。我々には、さらなる秘術が待っている。mutexは複数の魔術が互いに干渉し混沌におちいる事を食い止める。selectは複数の魔法通信路に最速の対応を行う。さらには効力範囲や持続時間を制御する高位の魔術とされるcontextも存在する。改めて記すが、強大な力には代償が伴う。安定させ正しく制御するには術者の莫大な精神力を必要とし、複雑に絡み合ったgoroutineは術者の精神を蝕む。しかし、goroutineによる並行処理の可能性は、その代償に十分に値する。他の神託機械では莫大な多重要求に応答することは夢見ることすらできなかったが、この神託機械は応える。神託機械の基底部分(OS)の機能を使わない軽量スレッドを持っているからだ。まさに選ばれた神託機械と言えよう。高レベルの術者となるためにはgoroutineを使いこなすための試練は避けられない。

7
3
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
7
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?