www.zhblog.net

Channel


在〈Goroutine〉中提到,想要通知主流程 Goroutine 已经结束,使用 Channel 是一种方式,实际上,Channel 是 Groutine 间的沟通管道。

使用 Channel

Channel 就像是个队列,可以对它发送值,也可以从它上头获取值,想要创建一个 Channel,要在类型之前加上个chan,每个chan都要定义可容纳的类型。

举例来说,使用 Channel 来修改之前的龟兔赛跑程序:

package main

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

func random(min, max int) int {
    rand.Seed(time.Now().Unix())
    return rand.Intn(max-min) + min
}

func tortoise(totalStep int, goal chan string) {
    for step := 1; step <= totalStep; step++ {
        fmt.Printf("乌龟跑了 %d 步...\n", step)
    }
    goal <- "乌龟"
}

func hare(totalStep int, goal chan string) {
    flags := [...]bool{true, false}
    step := 0
    for step < totalStep {
        isHareSleep := flags[random(1, 10)%2]
        if isHareSleep {
            fmt.Println("兔子睡着了zzzz")
        } else {
            step += 2
            fmt.Printf("兔子跑了 %d 步...\n", step)
        }
    }
    goal <- "兔子"
}

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

    totalStep := 10

    go tortoise(totalStep, goal)
    go hare(totalStep, goal)

    fmt.Printf("%s 抵达终点\n", <-goal)
    fmt.Printf("%s 抵达终点\n", <-goal)
}

在这个范例中,使用make创建了一个 Channel,当乌龟或兔子抵达终点时,使用goal <-发送一个字符串至 Channel 中,而在主流程中,使用<- goal从 Channel 获取字符串,若 Channel 中无法获取数据,这时会发生阻断,直到可从 Channel 中获取字符串为止。实际上,使用goal <-发送数据至 Channel 时,若 Channel 中已有数据,也会发生阻断,直到该数据被取走为止。

Buffered Channel

上头的范例创建 Channel 时并没有指定 Channel 中可以容纳多少数据,Channel 中默认只能容纳一个数据,你可以在创建 Channel 时指定当中可以容纳的数据数量。例如,创建一个生产者、消费者的程序:

package main

import "fmt"

func producer(clerk chan int) {
    fmt.Println("生产者开始生产整数......")
    for product := 1; product <= 10; product++ {
        clerk <- product
        fmt.Printf("生产了 (%d)\n", product)
    }
}

func consumer(clerk chan int) {
    fmt.Println("消费者开始消耗整数......")
    for i := 1; i <= 10; i++ {
        fmt.Printf("消费了 (%d)\n", <-clerk)
    }
}

func main() {
    clerk := make(chan int, 2)

    go producer(clerk)
    consumer(clerk)
}

在这个程序中,创建的 Channel 的容量为 2,因此在 Channel 的容量未满前,发送数据至 Channel 并不会发生阻断。

close 与 range

在这篇文件的第一个范例中,由于预期只会从 Channel 中收到两个字符串,因此主流程中使用了两次<- goal,然而有时,我们无法事先知道,能从 Channel 得到几笔数据。

举例来说,你也许想写个猜数字游戏,在随机猜测数字的情况下,你无法事先知道要猜几次才会猜中,而你想将先前猜测的数字透过 Channel 传送:

package main

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

func random(min, max int) int {
    rand.Seed(time.Now().Unix())
    return rand.Intn(max-min) + min
}

func guess(n int, ch chan int) {
    for {
        number := random(1, 10)
        ch <- number
        if number == n {
            close(ch)
        }
        time.Sleep(time.Second)
    }
}

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

    go guess(5, ch)

    for i := range ch {
        fmt.Println(i)
    }

    fmt.Println("I hit 5....Orz")
}

在这个范例中,每次猜测的数字,都会使用ch <- number传至 Channel 中,而最后猜中数字时,使用close()关闭 Channel,Go 的range可以搭配 Channel 使用,在 Channel 尚未关闭前,搭配for就可以持续从 Channel 中取出数据。

select

如果有多个 Channel 需要协调,可以使用select,直接来看个多个生产者与一个消费者的例子:

package main

import "fmt"

func producer(clerk chan int) {
    fmt.Println("生产者开始生产整数......")
    for product := 1; product <= 10; product++ {
        clerk <- product
        fmt.Printf("生产了 (%d)\n", product)
    }
}

func consumer(clerk1 chan int, clerk2 chan int) {
    fmt.Println("消费者开始消耗整数......")
    for i := 1; i <= 20; i++ {
        select {
        case p1 := <-clerk1:
            fmt.Printf("消费了生产者一的 (%d)\n", p1)
        case p2 := <-clerk2:
            fmt.Printf("消费了生产者二的 (%d)\n", p2)
        }

    }
}

func main() {
    clerk1 := make(chan int)
    clerk2 := make(chan int)

    go producer(clerk1)
    go producer(clerk2)

    consumer(clerk1, clerk2)
}

selectcase中,会监看哪个 Channel 可以获取数据(或发送数据至 Channel),如果都有数据的话,就会随机选取,如果都无法获取数据(或发送数据至 Channel)就会发生 panic,这可以设置default来解决,也就是监看的 Channel 中都没有数据的话就会执行,或者利用select来做些超时设定。例如:

package main

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

func random(min, max int) int {
    rand.Seed(time.Now().Unix())
    return rand.Intn(max-min) + min
}

func producer(clerk chan int) {
    fmt.Println("生产者开始生产整数......")
    for product := 1; product <= 10; product++ {
        time.After(time.Duration(random(1, 5)) * time.Second)
        clerk <- product
        fmt.Printf("生产了 (%d)\n", product)
    }
}

func consumer(clerk1 chan int, clerk2 chan int) {
    fmt.Println("消费者开始消耗整数......")
    for i := 1; i <= 20; i++ {
        select {
        case p1 := <-clerk1:
            fmt.Printf("消费了生产者一的 (%d)\n", p1)
        case p2 := <-clerk2:
            fmt.Printf("消费了生产者二的 (%d)\n", p2)
        case <-time.After(3 * time.Second):
            fmt.Printf("消费者抱怨中…XD")
        }

    }
}

func main() {
    clerk1 := make(chan int)
    clerk2 := make(chan int)

    go producer(clerk1)
    go producer(clerk2)

    consumer(clerk1, clerk2)
}

如果过了 3 秒钟,另两个 Channel 都还是阻断,case <- time.After(3 * time.Second)该行就会成立,因此就可以看到消费者的抱怨了…XD

select中若有相同的 Channel,会随机选取。例如底下会显示哪个结果是不一定的:

package main

import "fmt"

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

    ch <- 1
    select {
    case <-ch:
        fmt.Println("随机任务 1")
    case <-ch:
        fmt.Println("随机任务 2")
    case <-ch:
        fmt.Println("随机任务 3")        
    }
}

单向 Channel

可以将 Channel 转为只可发送或只可取值的 Channel,例如:

package main

import "fmt"

func producer(clerk chan<- int) {
    fmt.Println("生产者开始生产整数......")
    for product := 1; product <= 10; product++ {
        clerk <- product
        fmt.Printf("生产了 (%d)\n", product)
    }
}

func consumer(clerk <-chan int) {
    fmt.Println("消费者开始消耗整数......")
    for i := 1; i <= 10; i++ {
        fmt.Printf("消费了 (%d)\n", <-clerk)
    }
}

func main() {
    clerk := make(chan int, 2)

    go producer(clerk)
    consumer(clerk)
}

clerk chan<- int是只能发送的 Channel,而clerk <-chan int是只能接收的 Channel,从一个只能发送的 Channel 接收数据,或者是对一个只能接收的 Channel 发送数据,都会引发 invalid operation 的错误。

透过 Channel 来作为 Goroutine 间的沟通机制,是 Go 中比较建议的方式,如果你真的不想要透过 Channel,而想要直接共用某些数据结构,就必须注意有无 Race condition的问题,若必要,可透过锁定资源的方式来避免相关问题,有关锁定的方式,可以参考sync.Mutex的使用。


展开阅读全文

评论

Hi,您需要填写昵称和邮箱!

  • 昵称 (必填)
  • 邮箱 (必填)
  • 心情