Golang 的 channel

Golang 的 Channel

简介

Golang 中的 channel 是一种非常重要的同步和通信机制,
它允许 goroutines 之间安全地传递数据。

Channel操作速查:

通道类型 \ 操作 关闭
nil chan 永久阻塞 永久阻塞 panic
已关 chan 可读 panic panic
活跃 chan 可读或阻塞 可写或阻塞 可关

注意: 从空通道中仅写入或仅读取数据会永远阻塞。

  1. channel 写入结束后,要关闭channel,否则空Channel读取数据会永远阻塞。

死锁(Deadlock)是并发编程中常见的一个问题,
它指的是多个进程或线程在执行过程中
因互相等待对方持有的资源而无法继续执行的状态。

浅尝

创建 Channel

ch := make(chan int)          // 创建一个无缓冲的整数通道
chBuf := make(chan int, 10)   // 创建一个缓冲大小为 10 的整数通道

发送和接收

ch <- 42        // 发送 42 到通道 ch
val := <-ch     // 从通道 ch 接收值
val, ok := <-ch // 读取通道 ch,同时返回值和 ok

关闭 Channel

一旦通道不再需要接收新的值,可以关闭它:

close(ch) // 关闭通道 ch

关闭后的通道不能再发送值,但是仍然可以从关闭的通道接收值,直到所有的值都被接收完毕。

判断 Channel 是否已关闭

val, ok := <-ch
if !ok {
    fmt.Println("通道已关闭")
}

如果通道已关闭且没有更多的值可以接收,<-ch 会返回零值,同时 ok 为 false。

分类

无缓通道

没有缓冲区的 chan ,在写入后会阻塞,直到有接收。
下面的例子中,仅写入,没有读,所以会永远阻塞,
不会执行到 fmt.Println("写入之后")

无缓:写一次,读一次,只读不写、只写不读都会阻塞

// 无缓,仅写入,协程中阻塞在写入行,进行不到下一行
func TestChanNoBuf(t *testing.T) {
    ch := make(chan int)

    go func() {
        fmt.Println("写入之前")
        ch <- 1
        fmt.Println("写入之后")
    }()

    time.Sleep(time.Second)
}

有缓通道

缓冲 channel 有一个固定的缓冲区,即使没有接收者的情况下存储一定数量的消息。
在缓冲区未满时,发送操作不会阻塞;
在缓冲区未空时,接收操作也不会阻塞。

chBuf := make(chan int, 2)
chBuf <- 1
chBuf <- 2 // 此时通道已满,再发送会阻塞

尝试


// 有缓
func TestChanBuf(t *testing.T) {

    ch := make(chan int, 5)

    go func() {
        for i := 0; i < 100; i++ {
            // time.Sleep(time.Second) // 造成慢写,此时快读,使读时阻塞
            fmt.Println("写入前", i)
            ch <- i
            fmt.Println("写入后", i)
            fmt.Println()
        }
        close(ch)
    }()

    for {
        fmt.Println("读前")
        // time.Sleep(time.Second * 2) // 造成慢读,此时写快,使写时阻塞
        d, ok := <-ch
        fmt.Println("读出", d, ok)
        if !ok {
            break
        }
    }
}

有缓:chan 可以被写入数量为缓冲区数量,
写满继续写,会阻塞,等待读。--写快读慢。
读空继续读,会阻塞,等待写。--写慢读快。
缓冲区有空未才可继续写。

其实就是 chan 的特性,读写都会阻塞,等待写读。

单向

仅读通道

定义一个仅读类型 接受者 Receiver

// 定义
type Receiver <-chan int
// 使用
receiver := Receiver(ch)

如何快速记住 <-chan int 呢?

chan<- 右边 就是读.

下面 向 ch 中写入10个数字,使用仅读的 receiver 读取

func TestChanReader(t *testing.T) {
    ch := make(chan int, 2)
    go func() {
        for i := 0; i < 10; i++ {
            ch <- i
        }
        close(ch)
    }()

    receiver := Receiver(ch)
    // receiver <- 1 // receiver 为仅读,不可写入,会有错误提示
    for {
        d, ok := <-receiver
        fmt.Println(d)
        if !ok {
            break
        }
    }

}

receiver 因为初始化时使用了 仅接收模式的 Receiver 作为类型,所以不能写

  • 提问:为什么 closech,receiver 也被关闭了呢?

    因为 chan 是引用类型,使用 ch 初始化 receiver,
    receiverch 指向同一个地址,所以关闭 ch , receiver 也会关闭。

仅写通道

定义一个仅写类型 发送者 Sender

// 定义
type Sender chan<- int
// 使用
sender := Sender(ch)

快速记住 chan<- int
chan 在 <- 左边 就是写.

下面使用 Sender 类型 发送数据

func TestChanSender(t *testing.T) {
    ch := make(chan int, 2)

    sender := Sender(ch)
    go func() {
        for i := 0; i < 10; i++ {
            sender <- i
        }
        close(sender)
    }()
    // <-sender // 此时 sender 为仅写,不能读,会有错误提示
    for {
        d, ok := <-ch
        fmt.Println(d, ok)
        if !ok {
            break
        }
    }
}

sender 因为初始化时使用了 仅接收模式的 Sender 作为类型,所以不能写

  • 提问:为什么 close 了 sender,ch 也被关闭了呢?
    同上。
    因为 chan 是引用类型,使用ch初始化sender,
    sender和ch指向同一个地址,所以关闭sender,ch也会关闭。
    而sender 因为初始化时使用了 仅发送模式的Sender作为类型,所以不能读

单向合璧

结合发送者 SenderReceiver 类型:


func TestChanSenderReceiver(t *testing.T) {
    ch := make(chan int, 1)

    receiver := Receiver(ch)
    sender := Sender(ch)
    // 写入
    go func() {
        for i := 0; i < 10; i++ {
            sender <- i
        }
        close(sender)
    }()

    // 读
    for {
        d, ok := <-receiver
        if !ok {
            break
        }
        fmt.Println("读出", d)
    }

}

双向

最普通的就是双向,比如

ch := make(chan int)

出错情况

nil 通道

对 nil 通道进行读写操作会导致当前 goroutine 永久阻塞,
最终可能触发死锁错误(如果所有 goroutine 都被阻塞)。

func TestChanNilRead(t *testing.T) {
    var ch chan int // ch 是 nil 通道
    defer close(ch) // panic: close of nil channel
    // 读
    go func() {
        defer func() {
            if r := recover(); r != nil {
                fmt.Println(r) // 此 协程中未发生panic,读时仅阻塞
            }
        }()
        fmt.Println("等待接收")
        fmt.Println(<-ch) // 读不出来,因为写入时阻塞

    }()

    // 写入
    go func() {
        defer func() {
            if r := recover(); r != nil {
                fmt.Println(r) // 此 协程中未发生panic,写入时仅阻塞
            }
        }()
        fmt.Println("等待写入")
        ch <- 1             // 尝试向 nil 通道发送数据
        fmt.Println("已经写入") // 这行不会执行
    }()

    // 若在主协程中阻塞,将发生死锁
    // 主协程等待
    time.Sleep(1 * time.Second)
    fmt.Println("主协程结束")
}
操作
空通道 永久阻塞 永久阻塞 panic

已关通道

// 关闭的 chan ,只能读,不能写入、关闭
func TestChanClosed(t *testing.T) {
    ch := make(chan int, 1)
    close(ch)

    // close(ch) // 重复关闭: panic: close of closed channel
    // ch <- 1 // 写: panic: send on closed channel

    d, ok := <-ch      // 读 没问题
    fmt.Println(d, ok) // 读: 零值0 false

}
操作
已关 chan 可读 panic panic

活跃通道

// 活跃的 chan 读写关没问题
func TestChanActive(t *testing.T) {
    ch := make(chan int, 1)

    ch <- 1            // 写
    d, ok := <-ch      // 读
    fmt.Println(d, ok) // 1 true
    close(ch)          // 关闭
}
操作
活跃通道 可读或阻塞 可写或阻塞 可关

Select

select 可以同时等待多个通道操作,包括发送和接收。

func TestChanSelect(t *testing.T) {
    // 初始化 chan 并写入
    ch := make(chan int, 10)
    go func() {
        for i := 0; i < 5; i++ {
            ch <- i
        }
        fmt.Println("已经关闭")
    }()

    // 计时器
    timer := time.NewTimer(time.Second * 10)
    for {
        time.Sleep(time.Millisecond * 500)
        select {
        case d, ok := <-ch: // 读
            fmt.Println(d, ok)
            if !ok {
                fmt.Println("chan 已关闭")
                return
            }
        case ch <- 123: // 写入 , 需确保 chan 不会 close
            fmt.Println("写入成功")
        case <-timer.C: // 超时
            fmt.Println("超时")
            // close(ch)
        default: // 无case触发
            fmt.Println("无操作")
            return
        }
    }
}

select 的特性

  1. 随机性:

    当多个 case 可以执行时,select 会随机选择其中一个执行。
    select 会公平地选择可执行的 case,而不是总是选择第一个可执行的 case。
    这种随机选择机制有助于避免死锁和其他并发问题。

  2. 阻塞性:

    如果所有 case 都不可执行,则 select 会阻塞,直到有一个 case 可以执行。
    比如 select {} ,会永远阻塞。

  3. default:

    如果提供了 default 分支,则在所有 case 都不可执行时执行 default 分支。

Range 迭代 Channel

可以使用 for range 循环迭代通道中的所有值,直到通道关闭:

func TestChanRange(t *testing.T) {
    ch := make(chan int, 1)
    go func() {
        for i := 0; i < 3; i++ {
            ch <- i
            fmt.Println("写入", i)
        }
        close(ch) // 写入完成后 close 是个好习惯
    }()

    // range
    for d := range ch {
        fmt.Println("读出", d)
    }
    fmt.Println("range 结束") // 可以执行到此行
}
  1. range 时也是读,所以当 chan 为空时会阻塞,直到有数据可读或通道关闭
    所以如果 chan 停止写入且未关闭,range 读完后会阻塞,并最终导致 死锁
  2. chan 关闭,range 读完会退出循环。
  3. chan 写入完成后 close 是个好习惯。

Channel 与并发

Fan-In:多个 goroutine 向同一个通道发送数据。
Fan-Out:一个 goroutine 向多个通道发送数据。
Worker Pool:使用通道来管理一组工作 goroutine。

Deadlock

当 goroutine 尝试向一个没有接收者的通道发送数据,或者尝试从一个没有发送者的通道接收数据时,会发生死锁。使用缓冲通道或确保有对应的接收者/发送者可以避免死锁。

总结

Channel 是 Golang 的并发编程中的核心组件之一,它提供了 goroutines 之间安全的数据交换机制。
通过合理使用通道,可以有效地实现并发控制和数据同步。理解和熟练掌握通道的使用对于编写高效、可靠的 Go 程序至关重要。

本文由 上传。


如果您喜欢这篇文章,请点击链接 Golang 的 channel 查看原文。


您也可以直接访问:https://www.fanfine.cn/blog/212

暂无评论

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇
下一篇