Golang的channel

Channel

channel概念

Channel 是Go中的一个核心类型,你可以把它看成一个管道。Channel是引用类型,操作符是箭头 <- 。

Channel 是 CSP 模式的具体实现,用于多个 goroutine 通讯。其内部实现了同步,确保并发安全。

Channel 是线程安全的,先进先出,多个goroutine同时访问,不需要加锁,channel是有类型的,一个整数的channel只能存放整数。

声明方式

//声明string类型的chan
var stringCh chan string
var mapCh chan map[int]string
//make定义 无缓冲channel
var ch1 chan int = make(chan int)
var ch2 = make(chan int)
ch3 := make(chan int)

//make定义 有缓冲的channel
var ch_buffer chan int = make(chan int, 10)
//只读channel
var ch_ro <-chan int
var ch_ro2 <-chan int = make(<-chan int, 10)
ch_ro3 := make(<-chan int, 10)

//只写channel
var ch_wo chan<- int
var ch2 chan<- int = make(chan<- int, 10)
var ch3 = make(chan<- int, 10)
ch4 := make(chan<- int, 10)

注意事项

缓冲区或者叫容量(capacity) 代表Channel容纳的最多的元素的数量,代表Channel的缓存大小。
如果没有设置容量,或者容量设置为0,说明Channel没有缓存,只有发送者(sender)和接受者(receiver)都准好了,它们的通讯(communication)才会发生Blocking(阻塞zu se)。如果设置了缓存,就有可能不发生阻塞,只有buffer满了之后发送者(sender)才会阻塞,而只有缓存空了后,receiver才会阻塞,一个nil channel 不会通信,往一个已经被close的channel中继续发送数据会导致run-time panic
往nil channel中发送数据会一直被阻塞着。

无缓冲的与有缓冲的channel有着重大差别,那就是无缓冲是同步的,有缓冲是非同步的。

  • 无缓冲chan:ch1 := make(chan int)

  • 有缓冲chan:ch2 := make(chan int, 1)

  • 无缓冲 ch1<-1 不仅仅是向ch1通道写入1,而是要一直等待有别的协程接手这个参数,那么ch1<-1 才会继续下去,要不然就一直阻塞着。

  • 有缓冲 ch2<-1 则不会阻塞,因为缓冲大小是1,只有当放第2个值的时候,第1个还没有被人拿走,这时候才会阻塞。

channel的操作

  • 写channel

    var ch = make(chan int,3)
    ch<-99
    ch<-98
    ch<-97
  • 读channel

    v,ok := <-ch
    if ok {
     fmt.Printf("读取chan:%v\n", v)
    }
  • 关闭channel

    
    close(ch)
    注意:关闭之后再往里写数据会引发panic错误 `panic: send on closed channel`

完整示例
```Golang

func main() {
    ch := make(chan int, 4) //声明
    ch <- 99 //写入
    ch <- 98
    ch <- 97
    ch <- 96
    close(ch) //关闭
    ch <- 95 //这里会引发panic
    //以下是读取
    for {
        val, ok := <-ch
        if ok == false { //false代表ch已经closed
            fmt.Println("chan is closed")
            break
        }
        fmt.Println(val)
    }
}

channel的blocking

默认情况下,发送和接收回一直阻塞着,直到另一方准备好。这种方式可以用来在gororutine中进行同步,而不必使用显示的锁或者条件变量。

package main

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) // 728 17
    go sum(s[len(s)/2:], c) //-940 -5
    x,y:= <-c,<-c       // receive from c 一直等到计算结果发送到channl中
    fmt.Println(x,y,x+y) // 17 -5 12
}

channel的Range

package main

import (
    "fmt"
    "time"
)

func main() {
    //开启一个goroutine
    go func() {
        time.Sleep(1 * time.Hour)
    }()

    //声明一个channel
    c := make(chan int)
    go func() {
        for i := 0; i < 10; i = i + 1 {
            c <- i //把i写入 channel
        }
        //close(c)
    }()

    //循环channel
    for i := range c {
        fmt.Println(i)
    }

    fmt.Println("Finished")
}

channel的select

  • select的语句选择一组可能的send操作和receive操作去处理。它类似switch,但是只能用来处理通讯(communication)操作。它的case可以是send语句,也可以是receive语句,亦或是default

receive 语句可以将值赋值给一个或者两个变量。它必须是一个receive操作

package main

import "fmt"

func fibonacci(c, quit chan int) {
    fmt.Println("3")
    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)
    //验证执行的时间
    fmt.Println("1")
    go func() {
        fmt.Println("4")
    }()

    go func() {
        for i := 0; i < 10; i++ {
            fmt.Println(<-c)
        }
        quit <- 0
    }()

    fmt.Println("2")
    fibonacci(c, quit)
}

如果有同时多个case去处理,比如同时又多个channel可以接收数据,那么Go会随机选择一个case(pseudo-random 伪随机)处理。如果没有case需要处理,则会选择default去处理,如果没有default case,则select语句会阻塞,直到某个case需要处理

channel的timeout

select有很重要的一个应用就是超时处理。因为上面我们提到,如果没有case需要处理,select语句就会一直阻塞着。这时就需要一个超时处理

package main

import (
    "fmt"
    "time"
)

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")
    }
}

结果输出timeout 1,它利用的是time.After方法
,它返回一个类型为<-chan Time的单向的channel,在指定的时间发送一个当前时间给返回的channel中。

channel的Timer and Ticker

timer是一个定时器,代表未来的一个单一事件,你可以告诉timer你要等待多长时间,它提供一个Channel,在将来的那个时间那个Channel提供了一个时间值

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

用timer.Stop来停止计时器

package main

import (
    "fmt"
    "time"
)

func main() {
    timer1 := time.NewTimer(time.Second * 2)
    go func() {
        <-timer1.C //这个C是一个time类型的只读channel
        fmt.Println("Timer 1 is expired")
    }()

    stop2 := timer1.Stop()

    if stop2 {
        fmt.Println("Timer 2 stopped")
    }
}

ticker是一个定时触发的计时器,它会以一个间隔(interval)往Channel发送一个事件(当前时间),而Channel的接收者可以以固定的时间间隔从Channel中读取事件。

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

    fmt.Println("啥都没有")
    time.Sleep(time.Second * 6)

输出结果,可见是相差500ms
Tick at 2021-12-24 23:57:14.4941116 +0800 CST m=+0.506302301
Tick at 2021-12-24 23:57:14.9944728 +0800 CST m=+1.006663501
Tick at 2021-12-24 23:57:15.4945139 +0800 CST m=+1.506704601
Tick at 2021-12-24 23:57:15.9958106 +0800 CST m=+2.008001301
Tick at 2021-12-24 23:57:16.4957878 +0800 CST m=+2.507978501
Tick at 2021-12-24 23:57:16.9940308 +0800 CST m=+3.006221501
Tick at 2021-12-24 23:57:17.4953294 +0800 CST m=+3.507520101
Tick at 2021-12-24 23:57:17.9943069 +0800 CST m=+4.006497601
Tick at 2021-12-24 23:57:18.4940752 +0800 CST m=+4.506265901
Tick at 2021-12-24 23:57:18.9949129 +0800 CST m=+5.007103601
Tick at 2021-12-24 23:57:19.4946122 +0800 CST m=+5.506802901

channel 同步

channel可以用在goroutine之间的同步

package main

import (
    "fmt"
    "time"
)

func worker(done chan bool) {
    time.Sleep(time.Second)
    done <- true
}

func main() {
    done := make(chan bool, 1)
    go worker(done)

    select {
    case <-done:
        fmt.Println("oh finished")
    }
}

main goroutine通过done channel等待worker完成任务。 worker做完任务后只需往channel发送一个数据就可以通知main goroutine任务完成。

发表评论

您的电子邮箱地址不会被公开。 必填项已用*标注