zhcn 编程语言 Golang Golang 接口 非公開: Go 中的无缓冲通道

Go 中的无缓冲通道

Go 语言中的无缓冲通道是指在接收值之前无法保存值的通道。这种类型的通道要求发送和接收 goroutine 都准备好同时完成发送和接收操作。

如果两个 goroutine 没有同时准备好,通道会导致第一个发送或接收的 goroutine 阻塞等待。这种向通道发送和从通道接收的交互本质上是同步的。这些操作中的任何一个都不能脱离另一个而存在。

阻塞是指由于某种原因数据没有到达,当前协程(线程)继续等待,直到满足某个条件才解除阻塞。

同步是指在两个或多个协程(线程)之间保持数据内容一致性的机制。

为了清楚起见,让我们看两个完整的示例。这两个示例都使用无缓冲通道在两个 goroutine 之间同步交换数据。

[示例1] 在一场网球比赛中,两名球员在他们之间来回传球。球员总是处于两种状态之一:等待接球或将球击向对手。如下面的代码所示,您可以使用两个 goroutine 来模拟网球比赛,并使用无缓冲通道来模拟球的往返行程。

 // このサンプルプログラムは、バッファのないチャネルを使って、
// 2つのゴルーチン間のテニスの試合をシミュレートする方法を示しています。
package main

import (
    "fmt"
    "math/rand"
    "sync"
    "time"
)

// プログラムの終了を待つためのwg
var wg sync.WaitGroup

func init() {
    rand.Seed(time.Now().UnixNano())
}

// mainは、すべてのGoプログラムのエントリーポイントです。
func main() {
    // バッファのないチャネルを作成する
    court := make(chan int)

    // 2つのゴルーチンを待つため、カウントを2に増やす
    wg.Add(2)

    // 2人の選手を開始する
    go player("Nadal", court)
    go player("Djokovic", court)

    // サーブを開始する
    court <- 1

    // ゲームの終了を待つ
    wg.Wait()
}

// playerは、テニスを打っている選手をシミュレートします。
func player(name string, court chan int) {
    // 関数を終了する際にDoneを呼び出して、main関数に作業が完了したことを通知します。
    defer wg.Done()

    for {
        // 玉が打たれてくるのを待ちます
        ball, ok := <-court
        if !ok {
            // チャネルが閉じられた場合、私たちは勝ちました
            fmt.Printf("Player %s Won\n", name)
            return
        }

        // ランダムな数字を選んで、その数字でボールを落とすかどうかを判断します。
        n := rand.Intn(100)
        if n%13 == 0 {
            fmt.Printf("Player %s Missed\n", name)

            // チャネルを閉じて、私たちは負けましたと示します。
            close(court)
            return
        }

        // 打球数を表示し、打球数を1つ増やします。
        fmt.Printf("Player %s Hit %d\n", name, ball)
        ball++

        // ボールを相手に打つ
        court <- ball
    }
} 

当您运行该程序时,输出将如下所示。

Player Nadal Hit 1
Player Djokovic Hit 2
Player Nadal Hit 3
Player Djokovic Missed

Player Nadal Won

代码说明如下:

  • 第 22 行创建了一个 int 类型的无缓冲通道,允许两个 goroutine 在击球时彼此同步。
  • 第 28 行和第 29 行创建了两个参与竞赛的 goroutine。此时,两个 goroutine 都被阻塞等待击球。
  • 在第 32 行,球被发送到通道,程序开始运行游戏,直到 goroutine 输掉游戏。
  • for 语句的无限循环位于第 43 行。这个循环包括玩游戏的过程。
  • 在第 45 行,goroutine 从通道接收数据。该数据用于指示球等待。此接收操作会锁定 goroutine,直到数据发送到通道。当通道的接收操作返回时。
  • 第 46 行检查 ok 标志是否为 false。如果该值为 false,则通道关闭并且游戏结束。
  • 在第 53 到 60 行中,生成一个随机数来确定 goroutine 是否会被球击中。
  • 如果 goroutine 错过了球,第 58 行将关闭通道。然后两个 goroutine 返回,执行延迟的 Done,然后程序退出。
  • 当球被击中时,第 64 行将球的值增加 1,第 67 行将球返回到通道以发送给另一个玩家。此时两个 goroutine 都被锁定,直到交换完成。

[示例 2] 使用不同模式、使用无缓冲通道以及在 goroutine 之间同步数据来模拟接力赛。在接力赛中,四名选手轮流绕跑道跑。 2、3、4 号选手必须从前面的选手手中接过接力棒才能出发。比赛最重要的环节就是接力棒的传递,接力棒必须同时传递。同步接力棒时,参加接力赛的两名运动员必须同时做好接力棒的准备。代码如下所示。

 // このサンプルプログラムは、バッファのないチャネルを使って
// 4つのgoroutineの間でリレー競走をシミュレートする方法を示します
package main

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

// プログラムの終了を待つためのwg
var wg sync.WaitGroup

// mainはすべてのGoプログラムのエントリーポイントです
func main() {
    // バッファのないチャネルを作成する
    baton := make(chan int)

    // 最後のランナーのためにカウントを+1する
    wg.Add(1)

    // 最初のランナーがバトンを持ちます
    go Runner(baton)

    // 競争を開始する
    baton <- 1

    // 競争が終了するのを待つ
    wg.Wait()
}

// Runnerは、リレー競走の一人のランナーをシミュレートします
func Runner(baton chan int) {
    var newRunner int

    // バトンを待っています
    runner := <-baton

    // ラップトラックを回り始めます
    fmt.Printf("Runner %d Running With Baton\n", runner)

    // 次のランナーを作成する
    if runner != 4 {
        newRunner = runner + 1
        fmt.Printf("Runner %d To The Line\n", newRunner)
        go Runner(baton)
    }

    // ラップトラックを回る
    time.Sleep(100 * time.Millisecond)

    // 競争が終了しましたか?
    if runner == 4 {
        fmt.Printf("Runner %d Finished, Race Over\n", runner)
        wg.Done()
        return
    }

    // バトンを次のランナーに渡す
    fmt.Printf("Runner %d Exchange With Runner %d\n",
        runner,
        newRunner)

    baton <- newRunner
} 

当您运行该程序时,输出将如下所示。

Runner 1 Running With Baton
Runner 1 To The Line
Runner 1 Exchange With Runner 2
Runner 2 Running With Baton
Runner 2 To The Line
Runner 2 Exchange With Runner 3
Runner 3 Running With Baton
Runner 3 To The Line
Runner 3 Exchange With Runner 4
Runner 4 Running With Baton
Runner 4 Finished, Race Over

代码说明如下:

  • 第17行创建了一个int类型的无缓冲通道接力棒,用于同步传递接力棒。
  • 第 20 行向 WaitGroup 添加 1,以便 main 函数等待直到最后一个运行程序完成。
  • 第 23 行创建了一个 goroutine,通知您第一个跑步者已到达赛道。
  • 第 26 行,接力棒传递给赛跑者,比赛开始。
  • 在第 29 行,主函数在 WaitGroup 中阻塞并等待最后一个跑步者完成比赛。
  • 在第 37 行,goroutine 对接力通道执行接收操作,表明它正在等待接力。
  • 第 46 行,当接力棒传递时,会创建新的运行者,直到 goroutine 成为第四个运行者,准备好接收下一个接力棒。
  • 在第 50 行中,跑步者绕跑道运行 100 毫秒。
  • 在第 55 行,当第四个跑步者完成比赛时,Done 被调用,WaitGroup 减 1,并且 goroutine 返回。
  • 第64行,如果该goroutine不是第四个跑步者,则接力棒会传递给下一个已经在等待的跑步者。此时,goroutine 被锁定,直到切换完成。

这两个示例都使用无缓冲通道来模拟网球和接力赛来同步 goroutine。该代码很容易阅读,因为代码的流程与现实世界中这两个活动的流程完全相同。

现在您已经了解了无缓冲通道的工作原理,下一节将介绍缓冲通道。

通俗易懂的讲解“Go语言的无缓冲通道”!您必须观看的最佳 2 个视频

【元Javaエンジニア】Go言語の将来性・できること・メリットは?
https://www.youtube.com/watch?v=9vWLNB9UJ68&pp=ygVGIEdvIOiogOiqnuOBruODkOODg-ODleOCoeODquODs-OCsOOBleOCjOOBpuOBhOOBquOBhOODgeODo-ODjeODqyZobD1KQQ%3D%3D
Go言語で何ができるの?どこで使われてる?現役エンジニアが解説
https://www.youtube.com/watch?v=ZtDMTl_s30s&pp=ygVGIEdvIOiogOiqnuOBruODkOODg-ODleOCoeODquODs-OCsOOBleOCjOOBpuOBhOOBquOBhOODgeODo-ODjeODqyZobD1KQQ%3D%3D
Go 语言中的无缓冲通道是指在接收值之前无法保存值的通道。这种类型的通道要求发送和接收 goroutine 都准备好同时完成发送和接收操作。

如果两个 goroutine 没有同时准备好,通道会导致第一个发送或接收的 goroutine 阻塞等待。这种向通道发送和从通道接收的交互本质上是同步的。这些操作中的任何一个都不能脱离另一个而存在。

阻塞是指由于某种原因数据没有到达,当前协程(线程)继续等待,直到满足某个条件才解除阻塞。

同步是指在两个或多个协程(线程)之间保持数据内容一致性的机制。

为了清楚起见,让我们看两个完整的示例。这两个示例都使用无缓冲通道在两个 goroutine 之间同步交换数据。

[示例1] 在一场网球比赛中,两名球员在他们之间来回传球。球员总是处于两种状态之一:等待接球或将球击向对手。如下面的代码所示,您可以使用两个 goroutine 来模拟网球比赛,并使用无缓冲通道来模拟球的往返行程。

 // このサンプルプログラムは、バッファのないチャネルを使って、
// 2つのゴルーチン間のテニスの試合をシミュレートする方法を示しています。
package main

import (
    "fmt"
    "math/rand"
    "sync"
    "time"
)

// プログラムの終了を待つためのwg
var wg sync.WaitGroup

func init() {
    rand.Seed(time.Now().UnixNano())
}

// mainは、すべてのGoプログラムのエントリーポイントです。
func main() {
    // バッファのないチャネルを作成する
    court := make(chan int)

    // 2つのゴルーチンを待つため、カウントを2に増やす
    wg.Add(2)

    // 2人の選手を開始する
    go player("Nadal", court)
    go player("Djokovic", court)

    // サーブを開始する
    court <- 1

    // ゲームの終了を待つ
    wg.Wait()
}

// playerは、テニスを打っている選手をシミュレートします。
func player(name string, court chan int) {
    // 関数を終了する際にDoneを呼び出して、main関数に作業が完了したことを通知します。
    defer wg.Done()

    for {
        // 玉が打たれてくるのを待ちます
        ball, ok := <-court
        if !ok {
            // チャネルが閉じられた場合、私たちは勝ちました
            fmt.Printf("Player %s Won\n", name)
            return
        }

        // ランダムな数字を選んで、その数字でボールを落とすかどうかを判断します。
        n := rand.Intn(100)
        if n%13 == 0 {
            fmt.Printf("Player %s Missed\n", name)

            // チャネルを閉じて、私たちは負けましたと示します。
            close(court)
            return
        }

        // 打球数を表示し、打球数を1つ増やします。
        fmt.Printf("Player %s Hit %d\n", name, ball)
        ball++

        // ボールを相手に打つ
        court <- ball
    }
} 

当您运行该程序时,输出将如下所示。

Player Nadal Hit 1
Player Djokovic Hit 2
Player Nadal Hit 3
Player Djokovic Missed

Player Nadal Won

代码说明如下:

  • 第 22 行创建了一个 int 类型的无缓冲通道,允许两个 goroutine 在击球时彼此同步。
  • 第 28 行和第 29 行创建了两个参与竞赛的 goroutine。此时,两个 goroutine 都被阻塞等待击球。
  • 在第 32 行,球被发送到通道,程序开始运行游戏,直到 goroutine 输掉游戏。
  • for 语句的无限循环位于第 43 行。这个循环包括玩游戏的过程。
  • 在第 45 行,goroutine 从通道接收数据。该数据用于指示球等待。此接收操作会锁定 goroutine,直到数据发送到通道。当通道的接收操作返回时。
  • 第 46 行检查 ok 标志是否为 false。如果该值为 false,则通道关闭并且游戏结束。
  • 在第 53 到 60 行中,生成一个随机数来确定 goroutine 是否会被球击中。
  • 如果 goroutine 错过了球,第 58 行将关闭通道。然后两个 goroutine 返回,执行延迟的 Done,然后程序退出。
  • 当球被击中时,第 64 行将球的值增加 1,第 67 行将球返回到通道以发送给另一个玩家。此时两个 goroutine 都被锁定,直到交换完成。

[示例 2] 使用不同模式、使用无缓冲通道以及在 goroutine 之间同步数据来模拟接力赛。在接力赛中,四名选手轮流绕跑道跑。 2、3、4 号选手必须从前面的选手手中接过接力棒才能出发。比赛最重要的环节就是接力棒的传递,接力棒必须同时传递。同步接力棒时,参加接力赛的两名运动员必须同时做好接力棒的准备。代码如下所示。

 // このサンプルプログラムは、バッファのないチャネルを使って
// 4つのgoroutineの間でリレー競走をシミュレートする方法を示します
package main

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

// プログラムの終了を待つためのwg
var wg sync.WaitGroup

// mainはすべてのGoプログラムのエントリーポイントです
func main() {
    // バッファのないチャネルを作成する
    baton := make(chan int)

    // 最後のランナーのためにカウントを+1する
    wg.Add(1)

    // 最初のランナーがバトンを持ちます
    go Runner(baton)

    // 競争を開始する
    baton <- 1

    // 競争が終了するのを待つ
    wg.Wait()
}

// Runnerは、リレー競走の一人のランナーをシミュレートします
func Runner(baton chan int) {
    var newRunner int

    // バトンを待っています
    runner := <-baton

    // ラップトラックを回り始めます
    fmt.Printf("Runner %d Running With Baton\n", runner)

    // 次のランナーを作成する
    if runner != 4 {
        newRunner = runner + 1
        fmt.Printf("Runner %d To The Line\n", newRunner)
        go Runner(baton)
    }

    // ラップトラックを回る
    time.Sleep(100 * time.Millisecond)

    // 競争が終了しましたか?
    if runner == 4 {
        fmt.Printf("Runner %d Finished, Race Over\n", runner)
        wg.Done()
        return
    }

    // バトンを次のランナーに渡す
    fmt.Printf("Runner %d Exchange With Runner %d\n",
        runner,
        newRunner)

    baton <- newRunner
} 

当您运行该程序时,输出将如下所示。

Runner 1 Running With Baton
Runner 1 To The Line
Runner 1 Exchange With Runner 2
Runner 2 Running With Baton
Runner 2 To The Line
Runner 2 Exchange With Runner 3
Runner 3 Running With Baton
Runner 3 To The Line
Runner 3 Exchange With Runner 4
Runner 4 Running With Baton
Runner 4 Finished, Race Over

代码说明如下:

  • 第17行创建了一个int类型的无缓冲通道接力棒,用于同步传递接力棒。
  • 第 20 行向 WaitGroup 添加 1,以便 main 函数等待直到最后一个运行程序完成。
  • 第 23 行创建了一个 goroutine,通知您第一个跑步者已到达赛道。
  • 第 26 行,接力棒传递给赛跑者,比赛开始。
  • 在第 29 行,主函数在 WaitGroup 中阻塞并等待最后一个跑步者完成比赛。
  • 在第 37 行,goroutine 对接力通道执行接收操作,表明它正在等待接力。
  • 第 46 行,当接力棒传递时,会创建新的运行者,直到 goroutine 成为第四个运行者,准备好接收下一个接力棒。
  • 在第 50 行中,跑步者绕跑道运行 100 毫秒。
  • 在第 55 行,当第四个跑步者完成比赛时,Done 被调用,WaitGroup 减 1,并且 goroutine 返回。
  • 第64行,如果该goroutine不是第四个跑步者,则接力棒会传递给下一个已经在等待的跑步者。此时,goroutine 被锁定,直到切换完成。

这两个示例都使用无缓冲通道来模拟网球和接力赛来同步 goroutine。该代码很容易阅读,因为代码的流程与现实世界中这两个活动的流程完全相同。

现在您已经了解了无缓冲通道的工作原理,下一节将介绍缓冲通道。

通俗易懂的讲解“Go语言的无缓冲通道”!您必须观看的最佳 2 个视频

【元Javaエンジニア】Go言語の将来性・できること・メリットは?
https://www.youtube.com/watch?v=9vWLNB9UJ68&pp=ygVGIEdvIOiogOiqnuOBruODkOODg-ODleOCoeODquODs-OCsOOBleOCjOOBpuOBhOOBquOBhOODgeODo-ODjeODqyZobD1KQQ%3D%3D
Go言語で何ができるの?どこで使われてる?現役エンジニアが解説
https://www.youtube.com/watch?v=ZtDMTl_s30s&pp=ygVGIEdvIOiogOiqnuOBruODkOODg-ODleOCoeODquODs-OCsOOBleOCjOOBpuOBhOOBquOBhOODgeODo-ODjeODqyZobD1KQQ%3D%3D