【Go并发】— goroutine间的通信桥梁channel

194 阅读6分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 6 月更文挑战」的第2天,点击查看活动详情

🎐 放在前面说的话

大家好,我是北 👧🏻

本科在读,此为日常捣鼓.

如有不对,请多指教,也欢迎大家来跟我讨论鸭 👏👏👏

今天是我们「Go并发」系列的第二篇:「goroutine间的通信桥梁channel」;

src=http___static001.geekbang.org_infoq_e0_e00710596b96c7f3f05f44604b783965.jpeg&refer=http___static001.geekbang.webp Let’s get it

一、channel概念

  • 单纯将函数并发执行意义不大,函数与函数间能交换数据才是并发执行函数的意义。
  • channel是Go中的一个核心类型,我们可以把它看成一个管道,通过它并发核心单元就可以发送或者接收数据进行通讯,即goroutine之间进行通信的重要桥梁
  • channel像传送带医院传输数据,遵循“先进先出”规则
  • 通过通信来实现共享内存和线程同步,而不是通过共享内存来实现线程间的通信。
    • 共享内存在不同的goroutine中容易发送竞态问题,为了保证数据交换的正确性,必须对内存进行加锁,这种做法可能会造成性能问题。
  • 同一个通道,发送操作之间是互斥的,接收操作之间也是互斥的。

二、创建channel

1. 创建非缓冲channel:

// 使用内建函数make创建channel
ch := make(chan int)

// 使用var声明channel
var ch chan int
  • channel 必须定义其传递的数据类型
  • 以上两种是双向通道,可以发送数据,也可以接收数据

非缓冲通道:程序中必须同时有不同的goroutine对非缓冲通道进行发送和接收操作,否则会造成阻塞

缓冲通道:可以在同一个goroutine 内接收容量范围内的数据,即使没有另外一个goroutine进行读取

2. 创建缓冲channel

通过缓存的使用,可以尽量避免阻塞,提供应用的性能。

// 创建缓冲大小为33的通道
chstr := make(chan string,33)

三、操作channel

通道具备发送、接收、和关闭三种操作。

1. 发送&接收

默认情况下,channel接收和发送的数据都是阻塞的除非两端俱全,促使goroutine同步简单化,不需要显示的lock。

channel <- value // 发送value到channel
<- channel // 接收并将之丢弃
x := <- channel // 从channel中接收数据,并赋值给x
x, ok := <- channel // 从channel中接收数据,并赋值给x,外加判断通道是否关闭或空

2. 通道关闭

channel支持内置close函数来关闭通道。关闭通道是非必要的,但是手动关闭通道是一个好习惯,并且利用关闭的动作,可以给接收方传递信号(关闭通道一般在发送端关闭)。 被关闭后的通道的特点:

  1. 被关闭后,再发送数据,将会出现panic
  2. 关闭后,接收数据,不会影响原有数据的接收,会一直获取数据直到通道为空
  3. 重复关闭,会panic
  4. 对已关闭且无数据的通道进行接收操作,得到的对应数据类型的零值

四、实践一下下

1. 综上 for ...range 循环取值

close()函数用于关闭channel,关闭后channel中若还有缓冲数据,仍然可以读取,但是无法再发送数据给已经关闭的channel

// 只写不读

func write(out chan<- int) {

    for i := 0; i < 10; i++ {

        out <- i

    }

    close(out)

}

  

// 只读不写

func read(in <-chan int) {

    for num := range in {

        fmt.Println(num)

    }

}

  

func main() {

    // 创建双向通道

    ch := make(chan int)
    

    // 新建协程,生成数据,写入channel

    go write(ch)

    // 从channel读取内容,打印

    read(ch)

}

打印(换行): 0 1 2 3 4 5 6 7 8 9

  • 如果把close(c)注释掉,程序会一直阻塞在for …… range那一行。
  • 如果channel c已经被关闭,继续往它发送数据会导致panic: send on closed channel:

2.判断channel关闭以及单项通道使用

在前面基础上,把main函数里双向通道改为单向通道,并判断通道是否关闭

单向通道,在通道未关闭期间,可以对原本要接收到的通道里的值进行人工干预,比如下面的栗子,我们对原本要接收到的数据,再乘以了它本身。

func main() {

    // 创建双向通道

    //ch := make(chan int)

    //创建单项通道

    ch1 := make(chan int)

    ch2 := make(chan int)

    // 新建协程,生成数据,写入channel

    go write(ch1)

    // 开启goroutine从ch1中接收数据,并将该值的平方发送到ch2中

    go func() {

        for {

            i, ok := <-ch1 // 通道关闭后再取值ok=false

            if !ok {

                break

            }

            ch2 <- i * i

        }

        close(ch2)

    }()

    // 从channel读取内容,打印

    read(ch2)

}

打印(换行): 0 1 4 9 16 25 36 49 64 81

3. select多路复用

在某些场景下,我们需要同时从多个通道接收数据,如果没有数据,接收可能发送阻塞。然后我们就会运用一下这种方法实现:

    for {

        data, ok := <-ch1

        data, ok := <-ch2

    }

可以从多个通道接收到数据,但是性能很差,故此时我们应该用内置select关键字,做同时响应多个通道的操作。

select语句选择一组可能的send操作和receive操作去处理。它类似switch,支持default,如果select没有一条语句可以执行,即所有的通道都被阻塞。

1)格式

select {

    case <-c1: //监听channel的读事件

        // Do something

    case data <- c2: //读事件

        // Do something

    case c3 <- data: //监控channel的写事件

        // Do something

    default:

        // Do something

    }

2)select优势

  • 提高代码可读性
  • 处理一个或多个channel的发送/接收操作
  • 没有case的select{}会一直等待,可用于阻塞main函数
    • 有default:select语句不会被阻塞,执行default后,程序的执行会从select语句中恢复,进入下一次轮询。比较消耗资源。
    • 没有default:select语句将被阻塞,直到至少有一个通信可以进行下去
  • select语句的多个case同时满足条件, 执行哪个case是随机的
  • 可以为select设置一个超时时间,当select超时时,可以完成一些其他工作。

3)满足多条件下的select{}

func main() {

    ch := make(chan string)

    c := make(chan int)

    // 向管道ch中放数据

    go func() {

        time.Sleep(time.Second * 1)

        fmt.Println("get into string work")

        ch <- "string"

    }()

    // 向管道c中放数据

    go func() {

        time.Sleep(time.Second * 1)

        fmt.Println("get into int work")

        c <- 3

    }()

    //使用select从管道ch, c中读数据

    select {

    case n := <-ch:

        fmt.Println("get value from ch...", n)

    case n := <-c:

        fmt.Println("get value from c...", n)

    }

    time.Sleep(time.Second * 2)

}

image.png

4)select{}检测channel的关闭时间

func main() {

    start := time.Now()

    c := make(chan int)

    go func() {

        time.Sleep(time.Second * 1)

        //c <- 1

        close(c)

    }()

    fmt.Println("Blocking on read...")

    select {

    case <-c:

        fmt.Printf("Unblocked %v later.\n", time.Since(start))

    }

}

打印:

Blocking on read...

Unblocked 1.0142148s later.

5)select{}处理超时

在channel准备的时间中,我们想利用这一个时间时,我们可以设置一个超时时间,等待channel准备过程中进行一些处理,栗子如下

func main() {

    var c <-chan int

    for {

        select {

        case <-c:

        case <-time.After(time.Second * 1):

            fmt.Println("Timed out Do something.")

        }

    }

}

打印:(每隔一秒打印一次)

Timed out Do something.

Timed out Do something.

...

🎉 放在后面的话

以上总结了一些channelselectfor...range等相互结合的用法,希望对大家有用。还有很多用法,等以后用到,再慢慢补充。