[Golang]-5 协程、通道及其缓冲、同步、方向和选择器
阅读原文时间:2023年07月09日阅读:2

目录

Go 协程 在执行上来说是轻量级的线程。

代码演示

import (
    "fmt"
    "time"
)

func f(from string) {
    for i := 0; i < 3; i++ {
        fmt.Println(from, ":", i)
        time.Sleep(100)
    }
}

func main() {

    // 假设我们有一个函数叫做 f(s)。我们使用一般的方式调并同时运行。
    f("direct")

    // 使用 go f(s) 在一个 Go 协程中调用这个函数。这个新的 Go 协程将会并行的执行这个函数调用。
    go f("goroutine")

    // 可以为匿名函数启动一个 Go 协程。
    go func(msg string) {
        for i := 0; i < 3; i++ {
            fmt.Println(msg, "-", i)
            time.Sleep(100)
        }
    }("going")

    // 现在这两个 Go 协程在独立的 Go 协程中异步的运行,所以我们需要等它们执行结束。
    // 这里的 Scanln 代码需要我们在程序退出前按下任意键结束。
    var input string
    fmt.Scanln(&input)
    fmt.Println("done:" + input)
}

输出:

direct : 0
direct : 1
direct : 2
going - 0
goroutine : 0
goroutine : 1
going - 1
going - 2
goroutine : 2
done:my

当我们运行这个程序时,将首先看到阻塞式调用的输出,然后是两个 Go 协程的交替输出。

这种交替的情况表示 Go 运行时是以异步的方式运行协程的。

通道 是连接多个 Go 协程的管道。你可以从一个 Go 协程将值发送到通道,然后在别的 Go 协程中接收。

func main() {
    // 使用 make(chan val-type) 创建一个新的通道。通道类型就是他们需要传递值的类型。
    messages := make(chan string)

    // 使用 channel <- 语法 发送 一个新的值到通道中。
    // 这里我们在一个新的 Go 协程中发送 "ping" 到上面创建的messages 通道中。
    go func() {
        messages <- "ping1"
        messages <- "ping2"
    }()

    // 使用 <-channel 语法从通道中 接收 一个值。
    // 这里将接收我们在上面发送的 "ping" 消息并打印出来。
    msg1 := <-messages
    fmt.Println(msg1)

    fmt.Println(<-messages)
    fmt.Println(<-messages)
}

输出

ping1
ping2
fatal error: all goroutines are asleep - deadlock!

我们运行程序时,通过通道,消息 "ping" 成功的从一个 Go 协程传到另一个中。

默认发送和接收操作是阻塞的,直到发送方和接收方都准备完毕。这个特性允许我们,不使用任何其它的同步操作,来在程序结尾等待消息 "ping"。

默认通道是 无缓冲 的,这意味着只有在对应的接收(<- chan)通道准备好接收时,才允许进行发送(chan <-)。

可缓存通道允许在没有对应接收方的情况下,缓存限定数量的值。

func main() {
    // 这里我们 make 了一个通道,最多允许缓存 3 个值。
    messages := make(chan string, 3)

    // 因为这个通道是有缓冲区的,即使没有一个对应的并发接收方,我们仍然可以发送这些值。
    messages <- "ping1"
    messages <- "ping2"
    messages <- "ping3"

    fmt.Println(<-messages)
    fmt.Println(<-messages)
    fmt.Println(<-messages)
}

输出

ping1
ping2
ping3

我们可以使用通道来同步 Go 协程间的执行状态。这里是一个使用阻塞的接受方式来等待一个 Go 协程的运行结束。

// 这是一个我们将要在 Go 协程中运行的函数。
// done 通道将被用于通知其他 Go 协程这个函数已经工作完毕。
func worker(done chan bool) {
    fmt.Print("working...")
    time.Sleep(time.Second)
    fmt.Println("done")

    // 发送一个值来通知我们已经完工啦。
    done <- true
}

func main() {
    // 运行一个 worker Go协程,并给予用于通知的通道。
    done := make(chan bool, 1)
    go worker(done)

    //程序将在接收到通道中 worker 发出的通知前一直阻塞。
    <-done
}

输出:

working...done

如果你把 <- done 这行代码从程序中移除,程序甚至会在 worker还没开始运行时就结束了。

当使用通道作为函数的参数时,你可以指定这个通道是不是只用来发送或者接收值。这个特性提升了程序的类型安全性。

// ping 函数定义了一个只允许发送数据的通道。尝试使用这个通道来接收数据将会得到一个编译时错误。
func ping(pings chan<- string, msg string) {
    pings <- msg
}

// pong 函数允许通道(pings)来接收数据,另一通道(pongs)来发送数据。
func pong(pings <-chan string, pongs chan<- string) {
    msg := <-pings
    pongs <- msg
}

func main() {
    pings := make(chan string, 1)
    pongs := make(chan string, 1)
    ping(pings, "passed message")
    pong(pings, pongs)
    fmt.Println(<-pongs)
}

输出:

passed message

Go 的通道选择器 让你可以同时等待多个通道操作。

Go 协程和通道以及选择器的结合是 Go 的一个强大特性。

import (
    "fmt"
    "time"
)

func main() {
    // 在我们的例子中,我们将从两个通道中选择。
    c1 := make(chan string)
    c2 := make(chan string)

    // 各个通道将在若干时间后接收一个值,这个用来模拟例如并行的 Go 协程中阻塞的 RPC 操作
    go func() {
        time.Sleep(time.Second * 1)
        c1 <- "one"
    }()
    go func() {
        time.Sleep(time.Second * 2)
        c2 <- "two"
    }()

    // 我们使用 select 关键字来同时等待这两个值,并打印各自接收到的值。
    for i := 0; i < 2; i++ {
        select {
        case msg1 := <-c1:
            fmt.Println("received", msg1)
        case msg2 := <-c2:
            fmt.Println("received", msg2)
        }
    }
}

输出:

received one
received two

我们首先接收到值 "one",然后就是预料中的 "two"了。

注意从第一次和第二次 Sleeps 并发执行,总共仅运行了两秒左右。