1. Home
  2. Docs
  3. golang
  4. goroutine和channel
  5. channel在GO中特性

channel在GO中特性

本文由DeepL翻译 原文地址见末尾

简介

在我上一篇名为 "并发性、Goroutines和GOMAXPROCS "的文章中,我为讨论通道问题创造了条件。我们讨论了什么是并发性,以及goroutines是如何发挥作用的。有了这个基础,我们现在可以理解通道的本质,以及它们如何被用来同步goroutines,以一种安全、不容易出错和有趣的方式共享资源。

什么是通道

通道是一种安全的消息队列,它具有控制任何试图在其上接收或发送的goroutine行为的智能。一个通道作为两个goroutine之间的管道,将同步交换通过它的任何资源。正是通道控制goroutine交互的能力创造了同步机制。当一个通道被创建时没有容量,它被称为无缓冲通道。反过来,一个有容量的通道被称为缓冲通道。

为了理解任何与通道交互的goroutine的同步行为,我们需要知道通道的类型和状态。根据我们使用的是无缓冲通道还是有缓冲通道,情况会有些不同,所以让我们分别讨论一下。

无缓冲通道

无缓冲通道没有容量,因此需要两个goroutine准备好进行任何交换。当一个goroutine试图发送一个资源到一个非缓冲通道,并且没有goroutine等待接收该资源时,该通道将锁定发送的goroutine并使其等待。当一个goroutine试图从一个没有缓冲的通道接收资源,并且没有goroutine等待发送资源时,通道将锁定接收goroutine并让它等待。

在上图中,我们看到一个两个goroutine使用无缓冲通道进行交换的例子。在第1步中,两个goroutine接近通道,然后在第2步中,左边的goroutine把手伸进通道或执行一个发送。在这一点上,该goroutine被锁定在通道中,直到交换完成。然后在第3步中,右边的协作程序将他的手伸入通道或执行一个接收。这个goroutine也被锁定在通道中,直到交换完成。在第4步和第5步中,交换已经完成,最后在第6步中,两个goroutine可以自由地移开他们的手,继续他们的工作。

同步是在发送和接收之间的互动中固有的。没有另一个就不会发生。无缓冲通道的本质就是保证同步。

缓冲通道

缓冲通道有容量,因此可以有一些不同的行为。当一个goroutine试图发送一个资源到一个缓冲通道,而该通道已经满了,该通道将锁定goroutine并使其等待,直到一个缓冲区可用。如果通道里有空间,就可以立即进行发送,而goroutine可以继续前进。当一个goroutine试图从一个有缓冲的通道中接收,而有缓冲的通道是空的,该通道将锁定goroutine并使其等待,直到有资源被发送。

在上图中,我们看到一个例子,两个goroutine独立地从一个缓冲通道中添加和删除项目。在第1步中,右边的goroutine正在从通道中移除一个资源或执行一个接收。在第2步中,右边的goroutine可以在左边的goroutine向通道添加新的资源时独立地移除资源。在第3步,两个goroutine同时从通道中添加和删除资源,在第4步,两个goroutine都完成了。

同步仍然发生在接收和发送的交互中,然而当队列有缓冲区可用时,发送将不会被锁定。当有东西要从通道上接收时,接收不会被锁定。因此,如果缓冲区满了或者没有东西可以接收,缓冲通道的行为就很像一个没有缓冲的通道。

接力赛

如果你曾经观看过田径比赛,你可能见过接力赛。在接力赛中,有四名运动员作为一个团队,以最快的速度在跑道上奔跑。比赛的关键是,每队每次只能有一名选手在跑。拿着接力棒的选手是唯一被允许跑步的人,而接力棒从选手到选手的交换是赢得比赛的关键。

让我们建立一个示例程序,使用四个goroutines和一个通道来模拟接力赛。这些goroutines将是比赛中的选手,而通道将被用来在每个选手之间交换接力棒。这是一个典型的例子,说明资源如何在goroutines之间传递,以及通道如何控制与之交互的goroutines的行为。

package main

import (
    "fmt"
    "time"
)

func main() {
    // Create an unbuffered channel
    baton := make(chan int)

    // First runner to his mark
    go Runner(baton)

    // Start the race
    baton <- 1

    // Give the runners time to race
    time.Sleep(500 * time.Millisecond)
}

func Runner(baton chan int) {
    var newRunner int

    // Wait to receive the baton
    runner := <-baton

    // Start running around the track
    fmt.Printf("Runner %d Running With Baton\n", runner)

    // New runner to the line
    if runner != 4 {
        newRunner = runner + 1
        fmt.Printf("Runner %d To The Line\n", newRunner)
        go Runner(baton)
    }

    // Running around the track
    time.Sleep(100 * time.Millisecond)

    // Is the race over
    if runner == 4 {
        fmt.Printf("Runner %d Finished, Race Over\n", runner)
        return
    }

    // Exchange the baton for the next runner
    fmt.Printf("Runner %d Exchange With Runner %d\n", runner, newRunner)
    baton <- newRunner
}

当我们运行样本程序时,我们得到以下输出。

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

程序一开始就创建了一个无缓冲的通道。

// Create an unbuffered channel
baton := make(chan int)

使用一个无缓冲通道,迫使两个goroutines在同一时间准备好进行接力棒的交换。这种需要两个goroutine都准备好的情况创造了有保障的同步。

如果我们看一下主函数的其余部分,我们会看到为比赛中的第一个选手创建了一个goroutine,然后将接力棒交给了这个选手。这个例子中的接力棒是一个整数值,在每个跑者之间传递。这个例子使用了一个sleep来让比赛在main终止和结束程序之前完成。

// Create an unbuffered channel
baton := make(chan int)

// First runner to his mark
go Runner(baton)

// Start the race
baton <- 1

// Give the runners time to race
time.Sleep(500 * time.Millisecond)

如果我们只关注Runner函数的核心部分,我们可以看到交接棒是如何进行的,直到比赛结束。Runner函数是作为一个goroutine为比赛中的每个选手启动的。每次启动一个新的goroutine时,通道都会被传入goroutine。该通道是交换的管道,所以当前的选手和等待下一个的选手都需要引用该通道。

func Runner(baton chan int)

每个跑者做的第一件事是等待换棒。这是用通道上的接收来模拟的。receiver立即锁定goroutine,直到接力棒被送入通道。一旦接力棒被送入通道,receive将被释放,goroutine将模拟下一个跑者在赛道上冲刺。如果第四个跑者正在跑,就不会有新的跑者进入比赛。如果我们还在比赛中,就会启动一个新的goroutine,用于下一个跑者。

// Wait to receive the baton
runner := <-baton

// New runner to the line
if runner != 4 {
    newRunner = runner + 1
    go Runner(baton)
}

然后我们进行休眠,模拟跑步者绕着跑道跑的一些时间。如果这是第四个跑者,goroutine在睡眠后终止,比赛结束。如果不是,就会发生交换接力棒的情况,并将其送入通道。有一个goroutine已经被锁定并等待这个交换。一旦接力棒被送入通道,交换就完成了,比赛继续。

// Running around the track
time.Sleep(100 * time.Millisecond)

// Is the race over
if runner == 4 {
    return
}

// Exchange the baton for the next runner
baton <- newRunner

结论

这个例子展示了一个真实世界的事件,即跑步者之间的接力赛,以一种模仿实际事件的方式来实现。这就是通道的魅力之一。代码的流动方式模拟了这些类型的交流在现实世界中如何发生。

现在我们已经了解了无缓冲和有缓冲的通道的性质,我们可以看看我们可以使用通道实现的不同的并发模式。并发模式允许我们在goroutines之间实现更复杂的交换,模拟现实世界的计算问题,如semaphores、生成器和多路复用器。


原文地址: https://www.ardanlabs.com/blog/2014/02/the-nature-of-channels-in-go.html

Was this article helpful to you? Yes No

How can we help?