golang中的channel

1. 概念

单纯的将函数并发执行是没有意义的,函数与函数之间需要交换数据才能提现并发执行函数的意义
虽然可以使用共享内存来进行数据的交换,但是在共享内存在不同的goroutine中容易发生竟态问题,
为了保证数据交换的正确性,必须使用互斥量对内存进行加锁,这种做法势必造成性能问题,

go语言的并发模型是CSP,提倡通过通信共享内存,而不是通过共享内存而实现通信

如果说goroutine是go程序并发的执行体,那么channel就是它们之间的连接,
channel是可以让一个goroutine发送一个特定值到另一个goroutine的通信机制

go语言中的通道(channel)是一种特殊的类型,通道像一个传送带、队列,总是遵循先进先出的原则,
保证收发数据的顺序,每一个通道都是一个具体类型的导管,也就是声明channel的时候需要为其指定元素的类型

通道类型的空值是nil,空接口也是nil
声明通道之后需要使用make函数初始化通道才能使用

通道有发送send,接收receive,关闭close三种操作
我们通过内置的close函数来关闭通道 close(chan)

关闭通道需要注意的事情:只有在通知接收方goroutine所有数据都发送完毕的时候才需要关闭通道,
通道是可以被垃圾回收机制回收的,它和关闭文件是不一样的,在结束操作之后关闭文件是必须要做的
而关闭通道是不必须的

关闭通道后有一下几点:
1. 对一个关闭的通道在发送值就会导致panic
2. 对一个关闭的通道进行接后,会一直接收值知道通道为空
3. 对于一个关闭的且没有值得通道进行接收,会返回对应类型的零值
4. 关闭一个已经关闭的通道会导致panic

使用无缓冲通道可以实现goroutine之间的同步,无缓冲通道又称为阻塞的通道



2. 无缓冲通道
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
package main
 
import (
    "fmt"
    "sync"
)
 
func recv(c chan int, wg *sync.WaitGroup) {
    defer wg.Done()
    fmt.Println("接收成功", <-c)
}
 
func main() {
    var wg sync.WaitGroup
    // 无缓冲通道实现goroutine之间的同步
    ch1 := make(chan int)
    wg.Add(1)
    go recv(ch1, &wg)  // 启动goroutine从通道接收值
    ch1 <- 10  // 无缓冲通道因为没有人接收,会阻塞在这里形成死锁
    fmt.Println("发送成功")
    wg.Wait()
    // 报错:fatal error: all goroutines are asleep - deadlock!
    // 原因:无缓冲通道只有在有人接收值得时候才能发送值
 
    /* 总结:
    无缓冲通道的发送操作会阻塞,直到另一个goroutine在该通道上执行接收操作,这时值才能发送成功,
    两个goroutine将继续执行,相反,如果接收方先执行,接收方的goroutine将阻塞,直到另一个goroutine在该通道上发送一个值
 
    使用无缓冲通道进行通信将导致发送和接收的goroutine同步化,因此无缓冲通道也被称为同步通道
    */
}

  

3. 有缓冲通道

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
package main
 
import "fmt"
 
func main() {
    ch := make(chan int, 1)  // 创建一个容量为1的有缓冲通道
    ch <- 10
    fmt.Println("发送成功")
 
    fmt.Println(len(ch), cap(ch))
 
    /*
    只要通道的容量大于0,该通道就是有缓冲通道
    len函数可以获取通道内元素的个数,cap函数可以获取通道的容量
    */
}

  

4. 关闭通道 close()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
package main
 
import "fmt"
 
func main() {
    //可以通过内置的close函数关闭channel,(如果你的管道不往里存值或取值的时候一定要记得关闭管道)
    c := make(chan int)
    go func() {
        for i := 0; i < 10; i++ {
            c <- i
        }
        close(c)
        // channel不需要通过close释放资源,只要没有goroutine持有channel,垃圾回收机制会回收掉该channel
    }()
 
    /* 方法一
    for{
        if ret, ok := <-c; ok {
            fmt.Println(ret)
        } else {
            break
        }
    }
    */
    // 注意:遍历通道获取数据时,通道必须得关闭,否则遍历到最后就会陷入死锁
    for ret := range c {
        fmt.Println(ret)
    }
 
    fmt.Println("main 结束了...")
}

  

5. 单向通道

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
package main
 
import "fmt"
 
func counter(c chan<- int) {
    // 单向通道只能发送
    for i := 0; i < 10; i++ {
        c <- i
    }
    close(c)
}
func squarer(c1 <-chan int, c2 chan<- int) {
    // 两个单向通道,c1只能接收,c2只能发送
    for {
        ret, ok := <-c1
        if !ok {break}
        c2 <- ret * ret
    }
    close(c2)
}
func printer(c <-chan int) {
    // 单向通道只能接收
    for v := range c {
        fmt.Println(v)
    }
}
 
func main() {
    // 单向通道:限制通道在函数中只能发送或只能接收
    ch1 := make(chan int)
    ch2 := make(chan int)
 
    go counter(ch1)
    go squarer(ch1, ch2)
    printer(ch2)
}

 1.chan<- int是一个只能发送的通道,可以发送但是不能接收;

2.<-chan int是一个只能接收的通道,可以接收但是不能发送。


在函数传参及任何赋值操作中将双向通道转换为单向通道是可以的,但反过来是不可以的。


6. 通道总结

channel常见的异常总结,如下图:

通道总结

注意:关闭已经关闭的channel也会引发panic。

 
posted @   专职  阅读(278)  评论(0编辑  收藏  举报
编辑推荐:
· 记一次.NET内存居高不下排查解决与启示
· 探究高空视频全景AR技术的实现原理
· 理解Rust引用及其生命周期标识(上)
· 浏览器原生「磁吸」效果!Anchor Positioning 锚点定位神器解析
· 没有源码,如何修改代码逻辑?
阅读排行:
· 全程不用写代码,我用AI程序员写了一个飞机大战
· DeepSeek 开源周回顾「GitHub 热点速览」
· 记一次.NET内存居高不下排查解决与启示
· MongoDB 8.0这个新功能碉堡了,比商业数据库还牛
· .NET10 - 预览版1新功能体验(一)
点击右上角即可分享
微信分享提示