Go语言基础之并发(三) – Channel

135次阅读

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

简介

channel 俗称管道,用于数据传递或数据共享,其本质是一个先进先出的队列,使用goroutine+channel进行数据通讯简单高效,同时也线程安全,多个goroutine可同时修改一个channel,不需要加锁。

channel 可分为三种类型:

  • 只读channel:只能读 channel 里面数据,不可写入

  • 只写channel:只能写数据,不可读

  • 一般channel:可读可写

Channel 使用

定义和声明

// 只读chan
var readOnlyChan <-chan int
// 只写chan
var writeOnlyChan chan<- int

// 读写channel
// 定义完成以后需要make来分配内存空间,不然使用会deadlock
var mychan  chan int
mychannel = make(chan int,10)

//或者
//定义只读的channel
read_only := make (<-chan int,10)
//定义只写的channel
write_only := make (chan<- int,10)
//可同时读写
read_write := make (chan int,10)

读写数据

需要注意的是:

  • 管道如果未关闭,在读取超时会则会引发 deadlock 异常
  • 管道如果关闭,进行写入数据会 pannic
  • 当管道中没有数据的时候,再行读取或读取到默认值,如 int 类型默认值是0
ch <- "wd"  //写数据

a := <- ch //读取数据

a, ok := <-ch  //优雅的读取数据

循环管道

需要注意的是:

  • 使用 range 循环管道,如果管道未关闭会引发 deadlock 错误。
  • 如果采用 for 死循环已经关闭的管道,当管道没有数据时候,读取的数据会是管道的默认值,并且循环不会退出。
package main

import (
    "fmt"
)

func main() {

    mychannel := make(chan int, 10)

    for i := 0; i < 10; i++ {
        mychannel <- i
    }

    close(mychannel) //关闭管道

    fmt.Println("data lenght: ", len(mychannel))

    for v := range mychannel { //循环管道
        fmt.Println(v)
    }

    fmt.Printf("data lenght:  %d", len(mychannel))

}

Go语言基础之并发(三) - Channel

带缓冲区channel和不带缓冲区channel

带缓冲区channel:定义声明时候制定了缓冲区大小(长度),可以保存多个数据。

不带缓冲区channel:只能存一个数据,并且只有当该数据被取出时候才能存下一个数据。

//不带缓冲区
ch := make(chan int)

//带缓冲区
ch := make(chan int ,10)

不带缓冲区示例

package main

import "fmt"

func test(c chan int) {
    for i := 0; i < 10; i++ {
        fmt.Println("send ", i)
        c <- i
    }
}

func main() {
    ch := make(chan int)

    go test(ch)

    for j := 0; j < 10; j++ {
        fmt.Println("get ", <-ch)
    }
}

Go语言基础之并发(三) - Channel

channel实现作业池

我们创建三个 channel,一个 channel 用于接受任务,一个 channel 用于保持结果,还有个 channel 用于决定程序退出的时候

package main

import (
    "fmt"
)

func Task(taskch, resch chan int, exitch chan bool) {
    defer func() { //异常处理
        err := recover()
        if err != nil {
            fmt.Println("do task error:", err)
            return
        }
    }()

    for t := range taskch { //  处理任务

        fmt.Println("do task :", t)

        resch <- t
    }

    exitch <- true //处理完发送退出信号

}

func main() {

    taskch := make(chan int, 20) //任务管道

    resch := make(chan int, 20) //结果管道

    exitch := make(chan bool, 5) //退出管道

    go func() {
        for i := 0; i < 10; i++ {
            taskch <- i
        }
        close(taskch)

    }()

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

        //启动5个goroutine做任务
        go Task(taskch, resch, exitch)
    }

    go func() {
        //等5个goroutine结束
        for i := 0; i < 5; i++ {
            exit := <-exitch
            fmt.Println("receive exit chan: ", exit)
        }
        close(resch)  //任务处理完成关闭结果管道,不然range报错
        close(exitch) //关闭退出管道
    }()

    for res := range resch {
        //打印结果
        fmt.Println("task res: ", res)
    }
}

Go语言基础之并发(三) - Channel

只读channel和只写channel

一般定义只读和只写的管道意义不大,更多时候我们可以在参数传递时候指明管道可读还是可写,即使当前管道是可读写的

package main

import (
    "fmt"
    "time"
)

// 只能向chan里写数据
func send(c chan<- int) {
    for i := 0; i < 10; i++ {
        c <- i
    }

}

// 只能取channel中的数据
func get(c <-chan int) {
    for i := range c {
        fmt.Println(i)
    }
}

func main() {
    c := make(chan int)
    go send(c)
    go get(c)
    time.Sleep(time.Second * 1)
}

Go语言基础之并发(三) - Channel

select多路复用

原理通过select + case加入一组管道,当满足(这里说的满足意思是有数据可读或者可写) select 中的某个case 时候,那么该 case 返回,若都不满足 case,则走 default 分支

package main

import (
    "fmt"
    "os"
)

func send(c chan int) {
    for i := 1; i < 10; i++ {
        c <- i
        fmt.Println("send data : ", i)
    }
}

func main() {

    resch := make(chan int, 20)
    strch := make(chan string, 10)

    go send(resch)
    strch <- "wd"
    for {
        select {

        case a := <-resch:
            fmt.Println("get data : ", a)
        case b := <-strch:
            fmt.Println("get data : ", b)
        default:
            fmt.Println("no channel actvie")
            os.Exit(0)
        }
    }
}

Go语言基础之并发(三) - Channel

channel频率控制

在对channel进行读写的时,go还提供了非常人性化的操作,那就是对读写的频率控制,通过time.Tick实现

package main

import (
    "time"

    "fmt"
)

func main() {

    requests := make(chan int, 5)

    for i := 1; i < 5; i++ {

        requests <- i

    }

    close(requests)

    limiter := time.Tick(time.Second * 1)

    for req := range requests {

        <-limiter

        fmt.Println("requets", req, time.Now()) //执行到这里,需要隔1秒才继续往下执行,time.Tick(timer)上面已定义
    }

}

Go语言基础之并发(三) - Channel

总结

下面的表格中总结了对不同状态下的通道执行相应操作的结果。

Go语言基础之并发(三) - Channel

注意:对已经关闭的通道再执行 close 也会引发 panic。

正文完
 
mervinwang
版权声明:本站原创文章,由 mervinwang 2023-03-01发表,共计3116字。
转载说明:除特殊说明外本站文章皆由CC-4.0协议发布,转载请注明出处。
文章搜索