关于golang:GOchannel

9次阅读

共计 5099 个字符,预计需要花费 13 分钟才能阅读完成。

1.channel 的作用

Channel 是 Go 语言中一个十分重要的类型,是 Go 里的第一对象。通过 channel,Go 实现了 通过通信来实现内存共享,实际上:(数据拷贝了一份,并通过 channel 传递,实质就是个队列)。Channel 是在多个 goroutine 之间传递数据和同步的重要伎俩。应用原子函数、读写锁能够保障资源的共享拜访平安,但应用 channel 更优雅。

channel 字面意义是“通道”,相似于 Linux 中的管道。申明 channel 的语法如下:

chan ch // 申明一个双向通道
chan<- ch // 申明一个只能用于发送的通道
<-chan ch // 申明一个只能用于接管的通道 COPY

ch <- v    // 发送值 v 到 Channel ch 中
v := <-ch  // 从 Channel ch 中接收数据,并将数据赋值给 v

// 就像 map 和 slice 数据类型一样, channel 必须先创立再应用:
ch := make(chan int)

单向通道的申明,用 <- 来示意,它指明通道的方向。你只有明确,代码的书写程序是从左到右就马上能把握通道的方向是怎么的。

因为 channel 是一个援用类型,所以 在它被初始化之前,它的值是 nil,channel 应用 make 函数进行初始化。能够向它传递一个 int 值,代表 channel 缓冲区的大小(容量),结构进去的是一个缓冲型的 channel;不传或传 0 的,结构的就是一个非缓冲型的 channel。

两者有一些差异:非缓冲型 channel 无奈缓冲元素,对它的操作肯定程序是“发送 -> 接管 -> 发送 -> 接管 -> ……”,如果想间断向一个非缓冲 chan 发送 2 个元素,并且没有接管的话,第一次肯定会被阻塞;对于缓冲型 channel 的操作,则要“宽松”一些,毕竟是带了“缓冲”光环。
对 chan 的发送和接管操作都会在编译期间转换成为底层的发送接管函数。

v, ok := <-ch

它能够用来查看 Channel 是否曾经被敞开了

2. 同步与异步

Channel 分为两种:带缓冲、不带缓冲。对不带缓冲的 channel 进行的操作实际上能够看作“同步模式”,带缓冲的则称为“异步模式”。

同步模式下,发送方和接管方要同步就绪,只有在两者都 ready 的状况下,数据能力在两者间传输(前面会看到,实际上就是内存拷贝)。否则,任意一方后行进行发送或接管操作,都会被挂起,期待另一方的呈现能力被唤醒。

异步模式下,在缓冲槽可用的状况下(有残余容量),发送和接管操作都能够顺利进行。否则,操作的一方(如写入)同样会被挂起,直到呈现相同操作(如接管)才会被唤醒。

个性:【Go – Channel 原理】

  • 给一个 nil channel 发送数据,造成永远阻塞
  • 从一个 nil channel 接收数据,造成永远阻塞
  • 给一个曾经敞开的 channel 发送数据,引起 panic
  • 从一个曾经敞开的 channel 接收数据,如果缓冲区中为空,则返回一个零值
  • 无缓冲的 channel 是同步的,而有缓冲的 channel 是非同步的

以上 5 个个性是死货色,也能够通过口诀来记忆:空读写阻塞,写敞开异样,读敞开空零

blocking
默认状况下,发送和接管会始终阻塞着,直到另一方筹备好。这种形式能够用来在 gororutine 中进行同步,而不用应用显示的锁或者条件变量。

如官网的例子中 x, y := <-c, <- c 这句会始终期待计算结果发送到 channel 中。

import "fmt"
func sum(s []int, c chan int) {
    sum := 0
    for _, v := range s {sum += v}
    c <- sum // send sum to c
}
func main() {s := []int{7, 2, 8, -9, 4, 0}
    c := make(chan int)
    go sum(s[:len(s)/2], c)
    go sum(s[len(s)/2:], c)
    x, y := <-c, <-c // receive from c
    fmt.Println(x, y, x+y)
}

Range
for …… range 语句能够解决 Channel。
for … range c {do} 这种写法相当于 if _, ok := <-c; ok {do}

func main() {go func() {time.Sleep(1 * time.Hour)
    }()
    c := make(chan int)
    go func() {
        for i := 0; i < 10; i = i + 1 {c <- i}
        close(c)
    }()
    for i := range c {fmt.Println(i)
    }
    fmt.Println("Finished")
}

range c 产生的迭代值为 Channel 中发送的值,它会始终迭代直到 channel 被敞开。下面的例子中如果把 close(c)正文掉,程序会始终阻塞在 for …… range 那一行(不会报错)。

select
select 语句抉择一组可能的 send 操作和 receive 操作去解决。它相似 switch, 然而只是用来解决通信 (communication) 操作。
它的 case 能够是 send 语句,也能够是 receive 语句,亦或者 default。
receive 语句能够将值赋值给一个或者两个变量。它必须是一个 receive 操作。
最多容许有一个 default case, 它能够放在 case 列表的任何地位,只管咱们大部分会将它放在最初。

import "fmt"
func fibonacci(c, quit chan int) {
    x, y := 0, 1
    for {
        select {
        case c <- x:
            x, y = y, x+y
        case <-quit:
            fmt.Println("quit")
            return
        }
    }
}
func main() {c := make(chan int)
    quit := make(chan int)
    go func() {
        for i := 0; i < 10; i++ {fmt.Println(<-c)
        }
        quit <- 0
    }()
    fibonacci(c, quit)
}

如果有同时多个 case 去解决, 比方同时有多个 channel 能够接收数据,那么 Go 会 伪随机 的抉择一个 case 解决(pseudo-random)。如果没有 case 须要解决,则会抉择 default 去解决,如果 default case 存在的状况下。如果没有 default case,则 select 语句会阻塞,直到某个 case 须要解决。

须要留神的是,nil channel 上的操作会始终被阻塞,如果没有 default case, 只有 nil channel 的 select 会始终被阻塞。

select 语句和 switch 语句一样,它不是循环,它只会抉择一个 case 来解决,如果想始终解决 channel,你能够在里面加一个有限的 for 循环:

for {
    select {
    case c <- x:
        x, y = y, x+y
    case <-quit:
        fmt.Println("quit")
        return
    }
}

timeout
select 有很重要的一个利用就是超时解决。因为下面咱们提到,如果没有 case 须要解决,select 语句就会始终阻塞着。这时候咱们可能就须要一个超时操作,用来解决超时的状况。
上面这个例子咱们会在 2 秒后往 channel c1 中发送一个数据,然而 select 设置为 1 秒超时, 因而咱们会打印出 timeout 1, 而不是 result 1。

import "time"
import "fmt"
func main() {c1 := make(chan string, 1)
    go func() {time.Sleep(time.Second * 2)
        c1 <- "result 1"
    }()
    select {
    case res := <-c1:
        fmt.Println(res)
    case <-time.After(time.Second * 1):
        fmt.Println("timeout 1")
    }
}

其实它利用的是 time.After 办法,它返回一个类型为 <-chan Time 的单向的 channel,在指定的工夫发送一个以后工夫给返回的 channel 中。

Timer 和 Ticker
咱们看一下对于工夫的两个 Channel。
timer 是一个定时器,代表将来的一个繁多事件,你能够通知 timer 你要期待多长时间,它提供一个 Channel,在未来的那个工夫那个 Channel 提供了一个工夫值。上面的例子中第二行会阻塞 2 秒钟左右的工夫,直到工夫到了才会继续执行。

timer1 := time.NewTimer(time.Second * 2)
<-timer1.C
fmt.Println("Timer 1 expired")

当然如果你只是想单纯的期待的话,能够应用 time.Sleep 来实现。

你还能够应用 timer.Stop 来进行计时器。

timer2 := time.NewTimer(time.Second)
go func() {
    <-timer2.C
    fmt.Println("Timer 2 expired")
}()
stop2 := timer2.Stop()
if stop2 {fmt.Println("Timer 2 stopped")
}

ticker 是一个定时触发的计时器,它会以一个距离 (interval) 往 Channel 发送一个事件(以后工夫),而 Channel 的接收者能够以固定的工夫距离从 Channel 中读取事件。上面的例子中 ticker 每 500 毫秒触发一次,你能够察看输入的工夫。

ticker := time.NewTicker(time.Millisecond * 500)
go func() {
    for t := range ticker.C {fmt.Println("Tick at", t)
    }
}()

相似 timer, ticker 也能够通过 Stop 办法来进行。一旦它进行,接收者不再会从 channel 中接收数据了。

close
内建的 close 办法能够用来敞开 channel。

func TestClose(t *testing.T) {go func() {time.Sleep(time.Hour)
    }()
    c := make(chan int, 10)
    c <- 1
    c <- 2
    close(c)
    c <- 3
}

然而从这个敞开的 channel 中岂但能够读取出已发送的数据,还能够一直的读取零值:

c := make(chan int, 10)
c <- 1
c <- 2
close(c)
fmt.Println(<-c) //1
fmt.Println(<-c) //2
fmt.Println(<-c) //0
fmt.Println(<-c) //0

然而如果通过 range 读取,channel 敞开后 for 循环会跳出:

c := make(chan int, 10)
c <- 1
c <- 2
close(c)
for i := range c {fmt.Println(i)
}

通过 i, ok := <- c 能够查看 Channel 的状态,判断值是零值还是失常读取的值。

c := make(chan int, 10)
close(c)
i, ok := <-c
fmt.Printf("%d, %t", i, ok) //0, false

同步
channel 能够用在 goroutine 之间的同步。
上面的例子中 main goroutine 通过 done channel 期待 worker 实现工作。worker 做完工作后只需往 channel 发送一个数据就能够告诉 main goroutine 工作实现。

import (
    "fmt"
    "time"
)

func worker(done chan bool) {time.Sleep(time.Second)
    // 告诉工作已实现
    done <- true
}

func main() {done := make(chan bool, 1)
    go worker(done)
    // 期待工作实现
    <-done
}

限度最大并发数

// 最大并发数为 2
limits := make(chan struct{}, 2)
for i := 0; i < 10; i++ {go func() {
        // 缓冲区满了就会阻塞在这
        limits <- struct{}{}
        do()
        <-limits
    }()}

总结一下 channel 敞开后 sender 的 receiver 操作。
如果 channel c 曾经被敞开, 持续往它发送数据会导致 panic: send on closed channel:

小结:
同步模式下,必须要使发送方和接管方配对,操作才会胜利,否则会被阻塞;异步模式下,缓冲槽要有残余容量,操作才会胜利,否则也会被阻塞。

简略来说,CSP 模型由并发执行的实体(线程或者过程或者协程)所组成,实体之间通过发送音讯进行通信,这里发送音讯时应用的就是通道,或者叫 channel。

CSP 模型的要害是关注 channel,而不关注发送音讯的实体。Go 语言实现了 CSP 局部实践,goroutine 对应 CSP 中并发执行的实体,channel 也就对应着 CSP 中的 channel。

参考资料

Go 语言的 CSP 模型
Go Channel 详解

正文完
 0