golang channel 系统学习

2021-04-16  本文已影响0人  流雨声

goroutine是个啥

使用golang的channel之前,我们需要先了解go的goroutine。
Go 语言支持并发,我们只需要通过 go 关键字来开启 goroutine 即可。
goroutine 是轻量级线程,相比线程开销更小,完全由 Go 语言负责调度,是 Go 支持并发的核心。
如下所示,在go中我们可以很方便的开启并发执行。

package main
import (
    "fmt"
    "time"
)

func main() {
    go fmt.Println("goroutine message")
    fmt.Println("main function message")
    time.Sleep(time.Second)     //休眠1s
}

channel是个什么

通道(channel)则是用来传递数据的一个数据结构。 大部分时候 channel 都是和 goroutine 一起配合使用。
通道可用于两个 goroutine 之间通过传递一个指定类型的值来同步运行和通讯。操作符 <- 用于指定通道的方向,发送或接收。如果未指定方向,则为双向通道。

chan T          // 可以接收和发送类型为 T 的数据, 定义时使用
chan<- float64  // 只可以用来发送 float64 类型的数据, 在函数参数中使用, 这样可以限定chan使用
<-chan int      // 只可以用来接收 int 类型的数据, 在函数参数中使用, 这样可以限定chan使用

golang channel 大小

go channel缓冲区的大小 len也可以作用于channel,代表现在channel缓冲区中还有多少数据没有读取.
示例如下

c:=make(chan int,20) 
fmt.Println("len:",len(c)) //0 c

len也可以作用于channel,代表现在channel缓冲区中还有多少数据没有读取.示例如下

    c:=make(chan int,20)
    fmt.Println("len:",len(c)) //0
    c<-1
    fmt.Println("len:",len(c)) //1
    c<-1
    fmt.Println("len:",len(c)) //2
    c<-1
    fmt.Println("len:",len(c)) //3
    <-c
    fmt.Println("len:",len(c)) //2

golang channel 有无缓冲的区别

无缓冲channel

我们可以使用如下的方式声明一个无缓冲区的channel。其中int代表这个通道传递的是int类型。除了int、string、float等基本类型外,channel传递的类型还可以是自定义的结构体或别名等。

c := make(chan int)     //声明一个int类型的无缓冲通道

type NewType uint8
c1 := make(chan  NewType)       //声明自定义类型的无缓冲通道

通道最基本的用法就是在多个协程之间传递消息。channel是线程安全的,即在使用过程中,有多个协程同时向一个channel发送数据,或读取数据是完全可行的,不需要额外的操作。
无缓冲的通道只有当发送方和接收方都准备好时才会传送数据,否则准备好的一方将会被阻塞。
我们来看如下这个例子:我们声明了一个无缓冲的channel,然后开启一个协程向这个channel发送数据,另外主线程则从这个channel中读取数据。我们观察程序的输出可以发现,在主线程休眠期间,协程是阻塞在发送向通道发送数据的地方,只有当主线程休眠结束开始从channel中读取数据时,协程才开始向下运行。同样的,当协程发送完第一个数据休眠时,主线程读取了第一个数据,准备从channel中读取第二个数据时会被阻塞,知道协程休眠结束向通道发送数据后才会继续运行。

package main

import (
    "fmt"
    "time"
)

func main() {
    c := make(chan int)     //声明一个int类型的无缓冲通道
    go func() {
        fmt.Println("ready to send in g1")
        c <- 1
        fmt.Println("send 1 to chan")
        fmt.Println("goroutine start sleep 1 second")
        time.Sleep(time.Second)
        fmt.Println("goroutine end sleep")
        c <- 2
        fmt.Println("send 2 to chan")
    }()

    fmt.Println("main thread start sleep 1 second")
    time.Sleep(time.Second)
    fmt.Println("main thread end sleep")
    i := <- c
    fmt.Printf("receive %d\n", i)
    i = <- c
    fmt.Printf("receive %d\n", i)
    time.Sleep(time.Second)
}

输出:


image

由于channel这种阻塞发送方和接收方的特性,所以我们在使用channel时要防止死锁的发生。很明显,如果我们在一个线程内向同一个channel同时进行读取和发送的操作,就会导致死锁。

package main

import (
    "fmt"
)

func main() {
    c := make(chan int)     //声明一个int类型的无缓冲通道
    c <- 1
    i := <- c
    fmt.Printf("receive %d\n", i)
}
image

有缓冲channel

我们可以通过如下方式声明一个有缓存的channel。有缓存的channel区别在于只有当缓冲区被填满时,才会阻塞发送者,只有当缓冲区为空时才会阻塞接受者。

c := make(chan int, 10)

观察如下的例子:我们声明了一个容量为2的有缓冲的channel。开启一个协程,这个协程会向这个channel连续发送4个数据,然后休眠5s,接着再向channel发送2个数据。而主线程则会从这个channel中读取数据,每次读取前会先休眠1s。通过观察程序输出,我们可以发现,协程首先向channel发送了2个数据后(0、1),被阻塞,因为这时主线程在进行1s的休眠。主线程休眠结束后,从channel中读取了第一个数据0,之后继续休眠1s。channel此时的又有了缓冲,于是协程又向channel发送了第三个数据2,而后再次因为channel的缓冲区已满而休眠。依次类推,直到协程将4个数据发送完成之后,开始进行了5s的休眠。而当主线程从channel读完第4个数据(3)之后,当准备再从channel中读取第五个数据时,由于channel为空,主线程作为接受者被阻塞。直到协程的5s休眠结束,再次向channel中发送数据后,主线程读取到数据而不被阻塞。

package main

import (
    "fmt"
    "time"
)

func main() {
    c := make(chan int, 2)      //声明一个int类型的有缓冲通道
    go func() {
        for i := 0; i < 4; i++ {
            c <- i
            fmt.Printf("send %d\n", i)
        }
        time.Sleep(5 * time.Second)
        for i := 4; i < 6; i++ {
            c <- i
            fmt.Printf("send %d\n", i)
        }
    }()

    for i := 0; i < 6; i++ {
        time.Sleep(time.Second)
        fmt.Printf("receive %d\n", <-c)
    }
}

输出:


image

关闭一个channel

c := make(chan int, 2)
close(c)
package main

func main() {
    c := make(chan int, 2)
    close(c)
    c <- 1
}
image
package main

import (
    "fmt"
    "time"
)

func main() {
    c := make(chan int, 2)
    go func() {
        c <- 1
        time.Sleep(time.Second)
        c <- 2
        time.Sleep(time.Second)
        close(c)
    }()

    for i := 0; i < 6; i++ {
        j, ok := <-c
        fmt.Printf("receive: %d, status: %t\n", j, ok)
    }
}
image

和select关键字及for循环配合使用

for range 语法

我们可以使用for循环,持续的从一个channel中接受数据,当channel为空时,for循环会被阻塞。当channel被关闭时,则会跳出for循环。
如下例子,协程向channel中循环发送数据,并在循环结束时关闭channel。主线程是使用for range语句从channel中读取数据,很明显可以观察到,当channel为空时,for循环会被阻塞,当channel为无缓冲的时候也是如此。当协程关闭channel后,主线程跳出了for循环。

package main

import (
    "fmt"
    "time"
)

func main() {
    c := make(chan int)
    go func() {
        for i := 0; i < 3; i++ {
            c <-i
            fmt.Printf("send %d\n", i)
            time.Sleep(time.Second)
        }
        fmt.Println("ready close channel")
        close(c)
    }()

    for i := range c {
        fmt.Printf("receive %d\n", i)
    }
    fmt.Println("quit for loop")
}
image

select语法

使用select语句可以在多个可供选择的channel中读取任意一个数据执行。如果没有任何一个channel可以读取数据,则线程会被阻塞住,直到可以从某一个channel中读取数据为止。
select语句不会循环如果需要循环读取,需要手动在select语句外加循环.
如下这个例子中,主线程使用select语句从c、c2任意一个channel中读取数据。两个协程分布向c,c2中发送数据,其中一个在1s后发送,另一个在2s后发送。可以看到主线程一开始无法从任何一个channel中读取到数据,处于阻塞状态。在1s时收到了c2的数据,然后就会继续往下运行。

package main

import (
    "fmt"
    "time"
)

func main() {
    c := make(chan int)
    c2 := make(chan int)
    go func() {
        time.Sleep(2 * time.Second)
        c <- 1
    }()
    go func() {
        time.Sleep(1 * time.Second)
        c2 <- 2
    }()

    select {
    case i := <-c:
        fmt.Printf("receive from c: %d\n", i)
    case i := <- c2:
        fmt.Printf("receive from c2: %d\n", i)
    }
}
image

select语句还可以用于发送方。如下例子中,主线程将随机挑选一个仍有缓冲区channel发送数据,如果缓冲区已满,则这个channel的case语句将会被阻塞。

package main

import (
    "fmt"
    "time"
)

func main() {
    c := make(chan int)
    c2 := make(chan int)
    go func() {
        fmt.Printf("receive from c: %d\n", <-c)
    }()
    go func() {
        fmt.Printf("receive from c2: %d\n", <-c2)

    }()
    time.Sleep(time.Second)
    select {
    case c <- 1:
        fmt.Printf("send c\n")
    case c2 <- 1:
        fmt.Printf("send c2\n")
    }
}

注意close 一个channel也可以使select语句不再阻塞

package main

import (
    "fmt"
    "time"
)

func main() {
    c := make(chan int)
    go func() {
        time.Sleep(2 * time.Second)
        close(c)
    }()

    select {
    case i := <-c:
        fmt.Printf("receive from c: %d\n", i)
    }
}

输出:


image

select配合default使用
使用default关键字。使用select语句时,我们可以使用default关键字。和switch类似,这是一个默认的分支。如果所有的channel都没有准备好(例如对于发送者所有的channel都缓存已满,或对于接受者所有channel的缓存已空),则程序会进入default分支的逻辑。
这是刚刚的select例子,唯一不同的是我们在select语句中加入了一个default分支。运行后可以发现,主线程没有等待任何一个协程发送数据,而是直接进入了default的逻辑

package main

import (
    "fmt"
    "time"
)

func main() {
    c := make(chan int)
    c2 := make(chan int)
    go func() {
        time.Sleep(2 * time.Second)
        c <- 1
    }()
    go func() {
        time.Sleep(1 * time.Second)
        c2 <- 2
    }()

    select {
    case i := <-c:
        fmt.Printf("receive from c: %d\n", i)
    case i := <- c2:
        fmt.Printf("receive from c2: %d\n", i)
    default:
        fmt.Println("default")
    }
}

输出:


image

使用time标准库中的channel
golang的time标准库里提供了一些定时发送数据的channel,可以帮助我们实现一些功能。
例如利用time.After()函数配合select语句使用,可以实现超时的功能。本质上是time.After()函数返回了一个channel并在我们设定的时间后向其发送一个数据。

package main

import (
    "fmt"
    "time"
)

func main() {
    c := make(chan int)
    go func() {
        time.Sleep(2 * time.Second)
        c <- 1
    }()

    select {
    case i := <-c:
        fmt.Printf("receive from c: %d\n", i)
    case <-time.After(time.Second):
        fmt.Println("timeout! ")
    }
}

time标准库中的time.NewTicker()函数返回一个带有channel的结构体,并定时向这个结构体中发送时间数据,以实现定时器的功能。

package main

import (
    "fmt"
    "time"
)

func main() {
    c := time.NewTicker(time.Second)
    for t := range c.C {
        fmt.Printf("receive t :%s\n", t)
    }
}

输出:


image

总结
通道(channel)则是用来传递数据的一个数据结构,除了传递基本类型的数据外还可以传递自定义的类型
channel有带缓冲区和不带缓冲区(相当于缓冲区容量为0)两种类型。当缓冲区已满时会阻塞发送者,当缓冲区已空时会阻塞接受者。channel是线程安全的。
使用close关键字可以关闭channel。向已关闭的channel的发数据会panic。已关闭的channel中仍然可以读取数据,可以通过接受ok参数判断是否是从一个已关闭的channel中读取的数据。
使用for range语法可以从channel中循环读取数据,若channel为空,则循环会被阻塞,关闭channel会跳出循环。
select case语句挑选一个能读取出数据(或发送数据)的channel继续执行。如果有多个channel满足条件,则挑选其中任意一个。如果所有的channel都被阻塞,则select语句会被阻塞。使用default关键字可以避免select语句被阻塞。关闭一个channel同样可以使select语句不再阻塞。
time标准库中提供了time.After()方法,返回一个定时发送数据的channel,可以和select语句配合实现超时的逻辑。time标准库中还提供了time.NewTicker()方法,返回一个带有channel的结构体,并定时向这个结构体中发送时间数据,可以实现定时器的功能。

上一篇下一篇

猜你喜欢

热点阅读