Go语言并发编程——Channel

Channel(通道)

Channel是用来实现goroutine之间通信的数据类型,该类型是go自带的唯一一个处理高并发十分安全的一个类型。

Channel创建

类似于数组和切片的创建,我们也可以使用make()函数来创建管道。

c := make(chan int,5)

make函数在创建通道时有两个参数,第二个为可选参数。第一参数chan int是类型,chan是管道类型关键字,int表示管道中传输的数据类型为int类型。

同样的如果通道中传输的数据是string类型的,如下:

c := make(chan string)

第二个参数表示通道最大容量,也就是通道最多能够缓冲的元素数量,该参数必须大于零,设置该参数的通道称为缓存通道

不设置第二个参数,默认通道最大容量为0,也就是不能进行缓存,这样的通道被称为非缓通道。缓存通道与缓存通道的区别下文会讲述。

数据的发送和接收

通道的发送数据和接受数据需要依靠左箭头:<- ,表示右端向左端传输数据。

c := make(chan int)
//发送数据,将 1 放入通道
c <- 1
//接收数据,取出通道中数据并且赋值给i
i := <- c

对于非缓冲通道,数据的发送和接收是要一起完成的,中间不允许存在其它操作。发送数据的goroutine在发送的数据未被接收之前,自己连同其它发送数据的goroutine都会进入阻塞状态。接收数据的gotoutine在接收到数据之前会连同其它接收的goroutine都会进入阻塞状态。

对于缓冲通道,相当于一个先进先出的队列,最先进入通道的数据一定是第一个被接收。发送数据时,将数据成功放入通道后,该通道不会进入阻塞状态,当队列装满后,想要进行发送数据的goroutine就会进入阻塞状态,当通道中出去一个元素,那么最先进行等待的goroutine就会把数据放入通道,不在阻塞。接收数据时,当通道为空时所有接收数据的goroutine就会进入阻塞状态。

注意: 当通道关闭后,如果使用接收表达式从通道获取值,会获取到对应的零值。所以常常会用到接收表达式的第二个bool类型返回值,用该值判断通道是否打开,true表示打开,false表示通道已经关闭。

通道操作引起的恐慌

  1. 对已经关闭的通道进行发送操作 ------ panic: send on closed channel
package main

import "fmt"

func task(c chan int) {
    c <- 1
}
func main() {
    c := make(chan int)
    go task(c)
    close(c)
    i := <-c
    fmt.Println(i)
}
  1. 再次关闭已经关闭的通道 ---- panic: close of closed channel
package main

import "fmt"

func task(c chan int) {
    c <- 1
}
func main() {
    c := make(chan int)
    go task(c)
    close(c)
    close(c)
}
  1. 对于nil通道(没有进行初始化的通道),发送和接受操作都不会引起恐慌,但是会造成死锁——fatal error: all goroutines are asleep - deadlock!

对于nil通道进行close操作,会出现恐慌 —— panic: close of nil channel

单向通道

上面在创建或者在传递通道时用的是chan int 类型,这是双向通道类型,既可以发送数据,也可以接受数据。除了双向通道,还有单向通道的存在:

chan<-int —— 只能接收数据

int<-chan —— 只能发送数据

单独使用单向通道并没有什么意义,但是go语言支持在函数或方法在传参时双向通道自动转型为单向通道,下面为例:

package main

import (
    "fmt"
    "time"
)
type notifier interface {
    SendInt(c chan<- string)
}
type send struct {
    message string
}

func (s send) SendInt(c chan<- string) {
    c <- s.message
}
func main() {
    s := send{message: "text......"}
    ch := make(chan string)
    s.SendInt(ch)
}

接口notifer中有一个参数为单向发送通道,限制了所有实现该接口的方法只能往通道中发送数据,而不能接收数据。

显然,合理使用单向通道,可以有效约束不同业务对通道的操作,避免越权使用和滥用,此外,也提高了代码的可读性,一看函数参数就可以判断出业务对通道的操作类型。

select选择语句的使用

select选择语句是go语言专门为通道设计的语法,它的用法与switch语句相似,只不过case 后面只能跟通道表达式,也可以是含有通道表达式的赋值语句或短变量声明。

    select {
    case chan1 <- 1:
        fmt.Println("chan1发送数据")
    case i := <-chan2:
        fmt.Println("chan2获取数据", i)
    default//默认子句,可以没有
    }

selet语句会选择一个未进入阻塞分支执行。

当所有case子句均处于阻塞状态,若有default子句就会执行该子句,若没有default子句,就会进入阻塞状态,直到有一个子句不在阻塞。

当有多个子句都未阻塞,那么会根据随机选择算法,执行其中一个子句。

从通道取出数据

我们常常通过range子句来从通道中取出数据,如下:

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

这里需要注意几个点:

  1. 当通道中没有数据时,通道关闭后循环才会结束,在通道关闭之前循环会一直阻塞在for那一行。

  2. 在通道关闭后,循环仍然可以取出通道中剩余数据。当剩余数据全部取出后,循环结束。

使用Channel实现生产消费者模式

package main

import (
    "fmt"
    "sync"
    "time"
)

type event struct {
    ch      chan int
    buffer  int
    handle  func(num int)
    timeOut time.Duration
}

func NewEvent(buffer int, timeOut time.Duration, handle func(num int)) *event {
    return &(event{
        ch:      make(chan int, buffer),
        handle:  handle,
        timeOut: timeOut,
    })
}

func (e *event) Producer(num int) {
    select {
    case e.ch <- num:
        e.handle(-1)
    case <-time.After(e.timeOut):
    }
}

func (e *event) Consumer() {
    for v := range e.ch {
        e.handle(v)
    }
}
func main() {
    var wg sync.WaitGroup
    event := NewEvent(10, 2*time.Second, func(num int) {
        if num == -1 {
            fmt.Println("生产者生产了产品")
        } else {
            fmt.Println("消费者消费了第", num, "件商品")
        }
    })
    wg.Add(1)
    go func() {
        for i := 0; i < 100; i++ {
            event.Producer(i)
        }
        close(event.ch)
        wg.Done()
    }()
    wg.Add(1)
    go func() {
        event.Consumer()
        wg.Done()
    }()
    wg.Wait()
}

你可能感兴趣的:(开发语言,go,高并发)