PHPer的Go之路 -- 协程(go)及信道(channel)

臭大佬 2019-12-27 22:40:05 1532
Go 
简介 PHPer的Go之路 -- 协程(go)及信道(channel)

Go协程

Go 协程是与其他函数或方法一起并发运行的函数或方法。Go 协程可以看作是轻量级线程。与线程相比,创建一个 Go 协程的成本很小。因此在 Go 应用中,常常会看到有数以千计的 Go 协程并发地运行。

代码:

package main

import (
    "fmt"
)

func hello() {
    fmt.Println("Hello world goroutine")
}
func main() {
    //协程
    go hello()
    fmt.Println("main function")
}

λ go run ct.go
main function

main()调用了 go hello() 之后,程序控制没有等待 hello 协程结束,立即返回到了代码下一行,打印 main function。接着由于没有其他可执行的代码,Go 主协程终止,于是 hello 协程就没有机会运行了。

  • 启动一个新的协程时,协程的调用会立即返回。与函数不同,程序控制不会去等待 Go 协程执行完毕。在调用 Go 协程之后,程序控制会立即返回到代码的下一行,忽略该协程的任何返回值。
  • 如果希望运行其他 Go 协程,Go 主协程必须继续运行着。如果 Go 主协程终止,则程序终止,于是其他 Go 协程也不会继续运行。

信道(channel)

信道如同管道中的水会从一端流到另一端,通过使用信道,数据也可以从一端发送,在另一端接收。
所有信道都关联了一个类型。信道只能运输这种类型的数据,而运输其他类型的数据都是非法的。
chan T 表示 T 类型的信道。
信道的零值为 nil。信道的零值没有什么用,应该像对 map 和切片所做的那样,用 make 来定义信道。

声明与关闭

//声明一个信道
var a chan int
//或者
a := make(chan int)
//关闭信道
close(a)

发送和接收

//箭头对于 a 来说是向外指的,因此我们读取了信道 a 的值,并把该值存储到变量 data。
data := <- a // 读取信道 a  
//箭头指向了 a,因此我们在把数据写入信道 a。
a <- data // 写入信道 a

//<-a 这行代码通过信道 a 接收数据,但并没有使用数据或者把数据存储到变量中。这完全是合法的。
 <- a

发送与接收默认是阻塞的

程序控制会在发送数据的语句处发生阻塞,直到有其它 Go 协程从信道读取到数据,才会解除阻塞。与此类似,当读取信道的数据时,如果没有其它的协程把数据写入到这个信道,那么读取过程就会一直阻塞着。

package main

import (
    "fmt"
    "time"
)

func hello(done chan bool) {
    fmt.Println("hello go routine is going to sleep")
    time.Sleep(4 * time.Second)
    fmt.Println("hello go routine awake and going to write to done")
    done <- true
}
func main() {
    done := make(chan bool)
    fmt.Println("Main going to call hello go goroutine")
    //开启 hello 协程
    go hello(done)
    //阻塞,等待数据,也就是上面go协程 done <- true完成,才运行下面的代码
    <-done
    fmt.Println("Main received data")
}

死锁

使用信道需要考虑的一个重点是死锁。当 Go 协程给一个信道发送数据时,照理说会有其他 Go 协程来接收数据。如果没有的话,程序就会在运行时触发 panic,形成死锁。
同理,当有 Go 协程等着从一个信道接收数据时,我们期望其他的 Go 协程会向该信道写入数据,要不然程序就会触发 panic。

package main

func main() {
    ch := make(chan int)
    ch <- 5
}

单向信道

只能发送或者接收数据的信道。

package main

import "fmt"

//传递一个接收信道
func sendData(sendch chan<- int) {
    sendch <- 10
}

func main() {
    //创建了一个双向信道
    cha1 := make(chan int)
    //改变信道
    go sendData(cha1)
    //输出信道的值
    fmt.Println(<-cha1)
}

把一个双向信道转换成唯送信道或者唯收(Receive Only)信道都是行得通的,但是反过来就不行。

for range 遍历信道

package main

import (
    "fmt"
)

func producer(chnl chan int) {
    for i := 0; i < 10; i++ {
        chnl <- i
    }
    close(chnl)
}
func main() {
    ch := make(chan int)
    go producer(ch)
    for {
        //检测信道是否关闭,
        v, ok := <-ch
        if ok == false {
            fmt.Println("ok为false,则表示信道关闭")
            break
        }
        fmt.Println("接收 ", v, ok)
    }
    //for range遍历
    for v := range ch {
        fmt.Println("接收 ",v)
    }
}

信道容量和长度

缓冲信道

声明信道的时候,其实第二个参数(capacity)—容量参数,如果接收数量大于capacity,会发生死锁(deadlock)。程序会在运行时触发 panic

//capacity默认为0,大于0的时候就称为缓存信道
ch := make(chan type, capacity)

长度和容量

容量代表信道能容纳元素数量,长度表示信道里面有几个元素。

package main

import (
    "fmt"
)

func main() {
    //创建缓冲容量为3的信道
    ch := make(chan int,3)
    //接收值(如果写入信道的数量大于3,会发生死锁(deadlock)。程序会在运行时触发 panic
    ch <- 1
    ch <-2
    fmt.Println("容量: ", cap(ch))
    fmt.Println("长度: ", len(ch))
    fmt.Println("发送的值: ", <-ch)
    fmt.Println("现在的长度: ", len(ch))
}