Go コルーチンが明らかに: 軽量、同時実行性、パフォーマンスの完璧な組み合わせ

Go コルーチンは、軽量で効率的な機能と組み合わせた同時プログラミングのための強力なツールを提供し、開発者にユニークなプログラミング エクスペリエンスをもたらします。この記事では、Go コルーチンの基本原理、同期メカニズム、高度な使用法、パフォーマンス、ベスト プラクティスについて詳しく説明し、読者に包括的で深い理解と応用に関するガイダンスを提供することを目的としています。

パブリック アカウント [TechLeadCloud] をフォローして、インターネット アーキテクチャとクラウド サービス テクノロジに関する全次元の知識を共有してください。著者は 10 年以上のインターネット サービス アーキテクチャ、AI 製品開発の経験、およびチーム管理の経験があり、復旦大学の同済大学で修士号を取得し、復丹ロボット知能研究所のメンバーであり、Alibaba Cloud によって認定された上級アーキテクトです。プロジェクト管理のプロフェッショナルであり、数億の収益を誇る AI 製品の研究開発を担当しています。

ファイル

1. Go コルーチンの概要

Go コルーチン (ゴルーチン) は Go 言語の同時実行ユニットで、従来のスレッドよりもはるかに軽量で、Go 言語の同時実行モデルのコア コンポーネントです。Go では、通常のオペレーティング システム スレッドのオーバーヘッドを気にすることなく、数千のゴルーチンを同時に実行できます。

Go コルーチンとは何ですか?

Go コルーチンは、他の関数またはメソッドと並行して実行される関数またはメソッドです。軽量のスレッドと同様のものと考えることができます。その主な利点は、開始および停止のオーバーヘッドが非常に小さく、従来のスレッドよりも効率的に同時実行を実現できることです。

package main

import (
    "fmt"
    "time"
)

func sayHello() {
    for i := 0; i < 5; i++ {
        time.Sleep(100 * time.Millisecond)
        fmt.Println("Hello!")
    }
}

func main() {
    go sayHello() // 启动一个Go协程
    for i := 0; i < 5; i++ {
        time.Sleep(150 * time.Millisecond)
        fmt.Println("Hi!")
    }
}

出力:

Hi!
Hello!
Hi!
Hello!
Hello!
Hi!
Hello!
Hi!
Hello!

プロセス:sayHello上記のコードでは、ループ内で「Hello!」を 5 回出力する関数を定義しました。main関数では、goキーワードを使用してsayHellogoroutine として開始します。その後、main「Hi!」をさらに 5 回出力します。sayHellogoroutineなのでループmainインと並行して実行されます。したがって、出力に表示される「Hello!」と「Hi!」の順序は変わる可能性があります。

Go コルーチンとスレッドの比較

  1. 起動オーバーヘッド: Go コルーチンの起動オーバーヘッドは、スレッドの起動オーバーヘッドよりもはるかに小さくなります。したがって、何千ものゴルーチンを簡単に起動できます。
  2. メモリ フットプリント: 各 Go コルーチンのスタック サイズは小さく (通常は数 KB の範囲) で始まり、必要に応じて拡大または縮小できますが、スレッドには通常、固定されたより大きなスタック メモリ (通常は 1MB 以上) が必要です。
  3. スケジューリング: Go コルーチンは、オペレーティング システムではなく Go ランタイム システムによってスケジュールされます。これは、Go コルーチン間のコンテキスト切り替えのコストが低いことを意味します。
  4. セキュリティ: Go コルーチンは、チャネルなどの同期メカニズムと組み合わせた簡素化された同時実行モデルを開発者に提供し、同時実行プログラムでよくあるエラーを削減します。

サンプルコード:

package main

import (
    "fmt"
    "time"
)

func worker(id int, ch chan int) {
    for {
        fmt.Printf("Worker %d received data: %d\n", id, <-ch)
    }
}

func main() {
    ch := make(chan int)

    for i := 0; i < 3; i++ {
        go worker(i, ch) // 启动三个Go协程
    }

    for i := 0; i < 10; i++ {
        ch <- i
        time.Sleep(100 * time.Millisecond)
    }
}

出力:

Worker 0 received data: 0
Worker 1 received data: 1
Worker 2 received data: 2
Worker 0 received data: 3
...

処理:この例では、同じチャネルからデータを受信するために 3 つのワーカー ゴルーチンを開始します。この関数ではmain、データをチャネルに送信します。チャネルにデータがあるたびに、ワーカー ゴルーチンの 1 つがそれを受信して​​処理します。goroutine は同時に実行されるため、どの goroutine がデータを受け取るかは不定です。

Go コルーチンの主な利点

  1. 軽量: 前述したように、Go コルーチンの起動オーバーヘッドとメモリ使用量は、従来のスレッドよりもはるかに小さくなります。
  2. 柔軟なスケジューリング: Go コルーチンは調整およびスケジュールされ、ユーザーは適切なタイミングでタスクを切り替えることができます。
  3. 簡素化された同時実行モデル: Go は、同時プログラミングをよりシンプルかつ安全にするために、さまざまなプリミティブ (チャネルやロックなど) を提供します。

全体として、Go コルーチンは開発者に効率的で柔軟かつ安全な同時実行モデルを提供します。同時に、Go の標準ライブラリは、並行プログラムの開発プロセスをさらに簡素化するための豊富なツールとパッケージを提供します。


2. Go コルーチンの基本的な使い方

Go では、コルーチンは並行プログラムを構築するための基礎です。コルーチンの作成は非常に簡単で、goキーワードを使用して開始できます。基本的な使用法とそれに関連する例をいくつか見てみましょう。

Go コルーチンを作成して開始する

Go コルーチンを開始するには、goキーワードに続いて関数呼び出しを使用するだけです。この関数は匿名または事前定義することができます。

サンプルコード:

package main

import (
    "fmt"
    "time"
)

func printNumbers() {
    for i := 1; i <= 5; i++ {
        time.Sleep(200 * time.Millisecond)
        fmt.Println(i)
    }
}

func main() {
    go printNumbers()  // 启动一个Go协程
    time.Sleep(1 * time.Second)
    fmt.Println("End of main function")
}

出力:

1
2
3
4
5
End of main function

プロセス:この例では、printNumbers1 から 5 までの数字を単純に出力する関数を定義します。main関数内では、goキーワードを使用してこの関数を新しい Go コルーチンとして開始します。main 関数は Go コルーチンと並行して実行されます。main 関数が Go コルーチンの実行が完了するまで待機するように、main 関数を 1 秒間スリープさせます。

匿名関数を使用して Go コルーチンを作成する

事前定義された関数を開始するだけでなく、匿名関数を使用して Go コルーチンを直接開始することもできます。

サンプルコード:

package main

import (
    "fmt"
    "time"
)

func main() {
    go func() {
        fmt.Println("This is a goroutine!")
        time.Sleep(500 * time.Millisecond)
    }()
    fmt.Println("This is the main function!")
    time.Sleep(1 * time.Second)
}

出力:

This is the main function!
This is a goroutine!

プロセス:この例では、main関数内で匿名関数を直接使用して Go コルーチンを作成します。匿名関数では、単純にメッセージを出力し、500 ミリ秒間スリープさせます。main 関数は最初にメッセージを出力し、Go コルーチンが実行を完了するのに十分な時間を確保するために 1 秒待機します。

Go コルーチンと main 関数

メイン関数 (main) が終了すると、実行ステータスに関係なく、すべての Go コルーチンが直ちに終了されることに注意してください。

サンプルコード:

package main

import (
    "fmt"
    "time"
)

func main() {
    go func() {
        time.Sleep(500 * time.Millisecond)
        fmt.Println("This will not print!")
    }()
}

処理プロセス:上記のコードでは、Go コルーチンはメッセージを出力する前に 500 ミリ秒間スリープします。ただし、この期間中に main 関数が終了しているため、Go コルーチンも終了するため、出力は表示されません。

要約すると、Go コルーチンの基本的な使用法は非常にシンプルで直感的ですが、すべての Go コルーチンが実行される前に main 関数が終了しないように注意する必要があります。


3. Goコルーチンの同期機構

同時プログラミングでは、複数のコルーチンがリソースを共有したり、効果的かつ安全に連携したりできるようにするための鍵となるのが同期です。Go は、この目標を達成するためにいくつかのプリミティブを提供します。

1. チャンネル

チャネルは、Go でデータを渡し、コルーチン間で実行を同期するための主な方法です。これらは、あるコルーチンでデータを送信し、別のコルーチンでデータを受信するメカニズムを提供します。

サンプルコード:

package main

import "fmt"

func sendData(ch chan string) {
    ch <- "Hello from goroutine!"
}

func main() {
    messageChannel := make(chan string)
    go sendData(messageChannel) // 启动一个Go协程发送数据
    message := <-messageChannel
    fmt.Println(message)
}

出力:

Hello from goroutine!

プロセス:という名前のチャネルを作成しますmessageChannel次に、Go コルーチンが開始され、sendData文字列が"Hello from goroutine!"このチャネルに送信されます。main 関数では、このメッセージをチャネルから受信し、出力します。

2.sync.WaitGroup

sync.WaitGroupコルーチンのグループが完了するのを待つ構造体です。待機する必要があるコルーチンの数を表すカウントを増加させ、各コルーチンが完了するたびにカウントを減少させることができます。

サンプルコード:

package main

import (
    "fmt"
    "sync"
    "time"
)

func worker(id int, wg *sync.WaitGroup) {
    defer wg.Done()
    fmt.Printf("Worker %d starting\n", id)
    time.Sleep(time.Second)
    fmt.Printf("Worker %d done\n", id)
}

func main() {
    var wg sync.WaitGroup

    for i := 1; i <= 5; i++ {
        wg.Add(1)
        go worker(i, &wg)
    }

    wg.Wait()
    fmt.Println("All workers completed.")
}

出力:

Worker 1 starting
Worker 2 starting
Worker 3 starting
Worker 4 starting
Worker 5 starting
Worker 1 done
Worker 2 done
Worker 3 done
Worker 4 done
Worker 5 done
All workers completed.

処理:worker完了までに 1 秒かかる作業タスクをシミュレートする という関数を定義します。この関数では、defer wg.Done()関数が終了するときにカウントが確実にデクリメントされるようにするために を使用しますWaitGroupこの関数ではmain、このようなワーカー コルーチンを 5 つ開始し、1 つ開始するたびに、 を使用してwg.Add(1)カウントをインクリメントします。wg.Wait()すべてのワーカー コルーチンがWaitGroup完了したことが通知されるまでブロックされます。

3. ミューテックスロック( sync.Mutex)

複数のコルーチンが共有リソースにアクセスする必要がある場合 (共有変数の更新など)、ミューテックス ロックを使用すると、同時に 1 つのコルーチンのみがリソースにアクセスできるようになり、データ競合を防ぐことができます。

サンプルコード:

package main

import (
    "fmt"
    "sync"
)

var counter int
var lock sync.Mutex

func increment() {
    lock.Lock()
    counter++
    lock.Unlock()
}

func main() {
    var wg sync.WaitGroup

    for i := 0; i < 1000; i++ {
        wg.Add(1)
        go func() {
            defer wg.Done()
            increment()
        }()
    }

    wg.Wait()
    fmt.Println("Final Counter:", counter)
}

出力:

Final Counter: 1000

プロセス:グローバル変数がありcounter、それを複数の Go コルーチンで同時にインクリメントしたいと考えています。一度に 1 つの Go コルーチンのみを更新できるようにするためにcounter、ミューテックス ロックを使用してlockアクセスを同期します。

これらは、Go コルーチン同期メカニズムのいくつかの基本的な方法です。これらを正しく使用すると、より安全で効率的な同時実行プログラムを作成できます。


4. Go コルーチンの高度な使用法

Go コルーチンの高度な使用法には、より複雑な同時実行パターン、エラー処理、およびコルーチン制御が含まれます。いくつかの一般的な高度な使用法とその具体的な応用例を見ていきます。

1.セレクター( select)

selectステートメントは、Go で複数のチャネルを処理する方法です。これにより、複数のチャネル操作を待機し、利用可能な操作の 1 つを実行できます。

サンプルコード:

package main

import (
    "fmt"
    "time"
)

func main() {
    ch1 := make(chan string)
    ch2 := make(chan string)

    go func() {
        time.Sleep(1 * time.Second)
        ch1 <- "Data from channel 1"
    }()

    go func() {
        time.Sleep(2 * time.Second)
        ch2 <- "Data from channel 2"
    }()

    for i := 0; i < 2; i++ {
        select {
        case msg1 := <-ch1:
            fmt.Println(msg1)
        case msg2 := <-ch2:
            fmt.Println(msg2)
        }
    }
}

出力:

Data from channel 1
Data from channel 2

プロセス: 2 つのチャネルch1と を作成しますch22 つの Go コルーチンはそれぞれこれら 2 つのチャネルにデータを送信しますが、スリープ時間は異なります。このステートメントではselect、2 つのチャネルのいずれかでデータの準備ができるのを待ってから、それを処理します。ch1データが最初に到着するため、そのメッセージが最初に出力されます。

2.タイムアウト処理

を使用するとselect、チャネル操作のタイムアウト処理を簡単に実装できます。

サンプルコード:

package main

import (
    "fmt"
    "time"
)

func main() {
    ch := make(chan string)

    go func() {
        time.Sleep(3 * time.Second)
        ch <- "Data from goroutine"
    }()

    select {
    case data := <-ch:
        fmt.Println(data)
    case <-time.After(2 * time.Second):
        fmt.Println("Timeout after 2 seconds")
    }
}

出力:

Timeout after 2 seconds

処理プロセス: Go コルーチンは、chデータを送信する前に 3 秒間スリープします。selectステートメントでは、このチャネルからのデータ、または 2 秒のタイムアウトを待機します。Go コルーチンはタイムアウトになる前にデータを送信しないため、タイムアウト メッセージが出力されます。

3.contextコルーチン制御を使用する

contextパッケージを使用すると、キャンセル信号、タイムアウト、その他の設定を複数のコルーチン間で共有できます。

サンプルコード:

package main

import (
    "context"
    "fmt"
    "time"
)

func work(ctx context.Context) {
    for {
        select {
        case <-ctx.Done():
            fmt.Println("Received cancel signal, stopping the work")
            return
        default:
            fmt.Println("Still working...")
            time.Sleep(1 * time.Second)
        }
    }
}

func main() {
    ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
    defer cancel()

    go work(ctx)

    time.Sleep(5 * time.Second)
}

出力:

Still working...
Still working...
Still working...
Received cancel signal, stopping the work

プロセス:この例では、タイムアウトが 3 秒のサンプルを作成しますcontextGo コルーチンは、workキャンセル信号を受信するかタイムアウトになるまで動作し続けます。3 秒後にcontextタイムアウトがトリガーされ、Go コルーチンがキャンセル信号を受信して​​動作を停止します。

これらの高度な使用法により、Go コルーチンに強力な機能が提供され、複雑な同時実行パターンと制御が可能になります。これらの高度なテクニックをマスターすると、より堅牢で効率的な Go 同時実行プログラムを作成するのに役立ちます。


5. Go コルーチンのパフォーマンスとベスト プラクティス

Go コルーチンは、同時プログラミングのための軽量ソリューションを提供します。ただし、パフォーマンス上の利点を最大限に活用し、よくある落とし穴を回避するには、いくつかのベスト プラクティスとパフォーマンスに関する考慮事項を理解する価値があります。

1. 同時実行数を制限する

Go コルーチンは軽量ですが、多数の Go コルーチンを制御せずに作成すると、メモリの枯渇やスケジューリングのオーバーヘッドの増加につながる可能性があります。

サンプルコード:

package main

import (
    "fmt"
    "sync"
)

func worker(id int, wg *sync.WaitGroup) {
    defer wg.Done()
    fmt.Printf("Worker %d started\n", id)
}

func main() {
    var wg sync.WaitGroup
    numWorkers := 1000

    for i := 1; i <= numWorkers; i++ {
        wg.Add(1)
        go worker(i, &wg)
    }

    wg.Wait()
    fmt.Println("All workers done")
}

出力:

Worker 1 started
Worker 2 started
...
Worker 1000 started
All workers done

プロセス:この例では、1000 個のワーカー Go コルーチンを作成します。この数は問題を引き起こすことはないかもしれませんが、より多くの Go コルーチンが制限なく作成されると、問題が生じる可能性があります。

2. 競合状態を回避する

複数の Go コルーチンが共有リソースに同時にアクセスする可能性があり、結果が不確実になる可能性があります。データの一貫性を確保するには、ミューテックスまたはその他の同期メカニズムを使用します。

サンプルコード:

package main

import (
    "fmt"
    "sync"
)

var (
    counter int
    mu      sync.Mutex
)

func increment(wg *sync.WaitGroup) {
    defer wg.Done()
    mu.Lock()
    counter++
    mu.Unlock()
}

func main() {
    var wg sync.WaitGroup

    for i := 0; i < 1000; i++ {
        wg.Add(1)
        go increment(&wg)
    }

    wg.Wait()
    fmt.Println("Final counter value:", counter)
}

出力:

Final counter value: 1000

プロセス:sync.Mutexカウンタをインクリメントするときに排他的アクセスを確保するために使用します。これにより、同時アクセス時のデータの一貫性が確保されます。

3. ワークプールモードを使用する

ワークプールモードは、Go コルーチンの過剰な作成を避けるために、タスクを実行するための固定数の Go コルーチンを作成する方法です。タスクはチャネルを通じて送信されます。

サンプルコード:

package main

import (
    "fmt"
    "sync"
)

func worker(tasks <-chan int, wg *sync.WaitGroup) {
    defer wg.Done()
    for task := range tasks {
        fmt.Printf("Worker processed task %d\n", task)
    }
}

func main() {
    var wg sync.WaitGroup
    tasks := make(chan int, 100)

    // Start 5 workers.
    for i := 0; i < 5; i++ {
        wg.Add(1)
        go worker(tasks, &wg)
    }

    // Send 100 tasks.
    for i := 1; i <= 100; i++ {
        tasks <- i
    }

    close(tasks)
    wg.Wait()
}

出力:

Worker processed task 1
Worker processed task 2
...
Worker processed task 100

プロセス:tasksチャネルからタスクを受け取る5 つのワーカー Go コルーチンを作成しました。このモードでは、同時実行数を制御し、Go コルーチンを再利用できます。

これらのベスト プラクティスに従うと、Go コルーチン コードがより堅牢になるだけでなく、システム リソースがより効率的に使用され、プログラムの全体的なパフォーマンスも向上します。


6. まとめ

コンピューティング技術の進歩に伴い、同時実行性と並列性が現代のソフトウェア開発における重要な要素となっています。最新のプログラミング言語として、Go 言語は、その組み込みの を通じて、goroutineシンプルで強力な同時プログラミング モデルを開発者に提供します。ただし、これまでの章で説明したように、その仕組み、同期メカニズム、高度な使用方法、パフォーマンスとベスト プラクティスを理解することが重要です。

この記事では、Go コルーチンの基本とその仕組みを学んだだけでなく、そのパフォーマンスを最大化する方法に関するいくつかの高度なトピックについても検討しました。重要な洞察は次のとおりです。

  1. 軽量かつ効率的: Go コルーチンは軽量のスレッドですが、その実装特性により、多数の同時シナリオでより効率的になります。
  2. 同期とコミュニケーション: Go の哲学は、「共有メモリを通じてコミュニケーションするのではなく、コミュニケーションを通じてメモリを共有する」です。これはその強力なchannelメカニズムに反映されており、多くの同時実行の問題を回避するための鍵でもあります。
  3. パフォーマンスとベスト プラクティス: ベスト プラクティスを理解して従うことは、コードの堅牢性を確保するだけでなく、パフォーマンスを大幅に向上させることもできます。

結局のところ、Go は同時実行を処理するための強力なツールとメカニズムを提供しますが、真の技術はそれらを正しく使用することにあります。ソフトウェアエンジニアリングでよく見られるように、ツールは単なる手段であり、本当の力は、ツールがどのように機能するかを理解し、正しく適用することにあります。

この記事が Go コルーチンの詳細かつ包括的な理解を提供し、同時プログラミングの取り組みに貴重な洞察と指針を提供できれば幸いです。クラウド サービス、インターネット サービス アーキテクチャ、その他の複雑なシステムでよく見られるように、同時実行性を真に習得することが、パフォーマンス、スケーラビリティ、応答性を向上させる鍵となります。

パブリック アカウント [TechLeadCloud] をフォローして、インターネット アーキテクチャとクラウド サービス テクノロジに関する全次元の知識を共有してください。著者は 10 年以上のインターネット サービス アーキテクチャ、AI 製品開発の経験、およびチーム管理の経験があり、復旦大学の同済大学で修士号を取得し、復丹ロボット知能研究所のメンバーであり、Alibaba Cloud によって認定された上級アーキテクトです。プロジェクト管理のプロフェッショナルであり、数億の収益を誇る AI 製品の研究開発を担当しています。

お役に立ちましたら、個人の WeChat 公開アカウント [TechLeadCloud] にご注目ください。AI およびクラウド サービスの研究開発に関する全次元の知識を共有し、TechLead としてのテクノロジーに対する私の独自の洞察についてお話します。TeahLead KrisChang、インターネットおよび人工知能業界で 10 年以上の経験、技術およびビジネス チームの管理で 10 年以上の経験、同済大学でソフトウェア エンジニアリングの学士号、復旦大学でエンジニアリング管理の修士号、Alibaba Cloud 認定シニア アーキテクトクラウドサービス、AI製品事業の売上高は数億元。

Microsoft、新しい「Windowsアプリ」 .NET 8を正式にGAリリース、最新LTSバージョン XiaomiはXiaomi Velaが完全にオープンソースであり、基盤となるカーネルはNuttXであることを正式に発表 Alibaba Cloud 11.12 障害の原因が明らかに:Access Key Service(アクセスKey) 例外 Vite 5 が正式にリリースされた GitHub レポート : TypeScript が Java に取って代わり、3 番目に人気のある言語になる Rust で Prettier を書き換えるために数十万ドルの報酬を提供 オープンソース作者に「プロジェクトはまだ生きていますか?」と尋ねる 非常に失礼で、失礼な バイトダンス: AI を使用して Linux カーネル パラメータ 演算子を自動的に調整する 魔法の操作: バックグラウンドでネットワークを切断し、ブロードバンド アカウントを無効化し、ユーザーに光モデムの変更を強制する
{{名前}}
{{名前}}

おすすめ

転載: my.oschina.net/u/6723965/blog/10111899