chan和goroutine泄露

go chan
chan 类型
  • <发送/写>型 chan<-
  • <接收/读>型 <-chan
  • 双向型 chan
chan 操作
  • 构造/初始化 make()
  • 关闭 close()
  • 判等 ==
  • <发送/写>数据 chan <- send_data
  • <接收/读>数据 recv_data := <- chan
  • chan 关闭或有数据,读操作不阻塞
  • chan 未关闭且无数据,读操作阻塞
package main

import "fmt"

func main() {

    //双向型 chan, 零值 nil
    var ch chan int
    //输入型 chan->
    var ci chan<- int
    //输出型 <-chan
    var co <-chan int
    //make()
    cc := make(chan int)//无缓冲通道
    cc = make(chan int, 10)//容量为10的通道
    ch = cc
    //双向型赋值给单向型正确
    ci = ch
    co = ch
    //单向型赋值给双向型错误
    //ch = ci //❌
    //ch = co //❌
    fmt.Println(ci, co, ch) //0xc00008c000 0xc00008c000 0xc00008c000

    //可以赋值 -> 类型兼容 -> 可以判等
    b1 := ch == cc
    b2 := ci == nil
    b3 := ch == ci
    b4 := ch == co
    //不可以赋值 -> 类型不兼容 -> 不可以判等
    // b5 := ci == co //❌
    fmt.Println(ci, co, cc, ch) //0xc00008c000 0xc00008c000 0xc00008c000 0xc00008c00
    fmt.Println(b1, b2, b3, b4) //true false true true

    //chan 发送/写
    ch <- 1
    //chan 接收/读
    out := <-ch
    fmt.Println(out) // 1

    ch <- 1
    //close(), chan 关闭
    close(ch) //关闭后读操作不阻塞
    //chan 关闭后,还有数据.读操作,返回 数据 和 true
    out1, ok1 := <-ch
    //chan 关闭后,没有数据.读操作,返回 零值 和 false
    out2, ok2 := <-ch
    fmt.Println(out1, ok1, out2, ok2) // 1 true 0 fals

    ch = make(chan int, 0)
    //chan 没关闭,无数据,读操作阻塞
    out = <-ch

}
无缓冲通道(同步通道)

无缓冲通道上的发送/接收操作都会阻塞,直到另一个goroutine在对应通道上执行接收/发送操作,这是值传递完成。
使用无缓冲通道进行的通信会导致发送和接收goroutine同步化,因此无缓冲通道也叫作同步通道。

package main

import (
    "fmt"
)

func main() {
    defer func() {
        fmt.Println("defer")
    }()

    done := make(chan string)
    go func() {
        str := "12312412412412"
        fmt.Println("done")
        done <- str
    }()

    s := <-done //等待goroutine发送
    fmt.Println(s)
}

单向通道类型
  • 双向通道转换为单向通道是允许的,反过来则不行
  • 使用内置的close函数可以用来关闭通道,不能向一个已经关闭的通道发送数据,否则会宕机。
  • 接收方可以通过ret, ok := <- channel通过bool值ok来判断通道是否关闭
  • 可以通过range循环语法来接收通道上的消息
package main

import (
    "fmt"
)

func counter(out chan<- int) {
    for i := 0; i < 100; i++ {
        out <- i
    }
    close(out)
}

func squareter(out chan<- int, in <-chan int) {
    for x := range in {
        out <- x * x
    }
    close(out)
}

func printer(in <-chan int) {
    for v := range in {
        fmt.Printf("%d ", v)
    }
}

func main() {
    ch1 := make(chan int)
    ch2 := make(chan int)

    go counter(ch1)
    go squareter(ch2, ch1)
    printer(ch2)
}

缓冲通道

make(chan int, size)时指定了通道的容量大小,即为缓冲通道。

  • 通道为满时,发送操作所在的goroutine将会阻塞,直到接收方goroutine执行接收操作,使通道留出可写空间
  • 通道为空时,接收操作所在的goroutine将会阻塞,知道发送方goroutine执行发送操作
  • 通道和goroutine的调度深度关联,将缓冲通道作为队列在单个goroutine中使用是个错误的选择,如果没有另外一个goroutine从通道中接收,发送者有被永久阻塞的风险。如果仅需要一个简单队列,可以使用slice创建一个。
goroutine泄露

Go 中的并发性是以 goroutine(独立活动)和 channel(用于通信)的形式实现的。处理 goroutine 时,程序员需要小心翼翼地避免泄露。如果最终永远堵塞在 I/O 上(例如 channel 通信),或者陷入死循环,那么 goroutine 会发生泄露。即使是阻塞的 goroutine,也会消耗资源,因此,程序可能会使用比实际需要更多的内存,或者最终耗尽内存,从而导致崩溃。让我们来看看几个可能会发生泄露的例子。然后,我们将重点关注如何检测程序是否受到这种问题的影响。

回顾一下 goroutine 终止的场景:
  • 当一个goroutine完成它的工作

  • 由于发生了没有处理的错误

  • 有其他的协程告诉它终止

goroutine泄露场景
  • 1、发送不接收
    如下所示,它只返回三个goroutine中第一个发送的消息
package main

import (
    "fmt"
    "math/rand"
    "runtime"
    "time"
)

func multiRequest() string {
    response := make(chan string)

    go func() {
        time.Sleep(time.Duration(rand.Intn(4)+1) * time.Millisecond)
        response <- "func1"
        fmt.Println("go func1")
    }()
    go func() {
        time.Sleep(time.Duration(rand.Intn(4)+1) * time.Millisecond)
        response <- "func2"
        fmt.Println("go func2")
    }()
    go func() {
        time.Sleep(time.Duration(rand.Intn(4)+1) * time.Millisecond)
        response <- "func3"
        fmt.Println("go func3")
    }()

    return <-response
}

func main() {
    defer func() {
        fmt.Println("the number of goroutines: ", runtime.NumGoroutine())
    }()

    rand.Seed(time.Now().Unix())

    fmt.Println(multiRequest())
}

输出:

说明main在退出时,还有三个goroutine存在(包括main goroutine)

  • 2、接收不发送
// leak 是一个有 bug 程序。它启动了一个 goroutine 阻塞接收 channel。当 Goroutine 正在等待时,leak 函数会结束返回。此时,程序的其他任何部分都不能通过 channel 发送数据,那个 channel 永远不会关闭,fmt.Println 调用永远不会发生, 那个 goroutine 会被永远锁死
func leak() {
     ch := make(chan int)

     go func() {
        val := <-ch
        fmt.Println("We received a value:", val)
    }()
}
  • 3、向已满的buffered channel发送,但是没有接收
    和第一种情况比较类似。
    在 channel 的接收值数量有限,且可以用 buffered channel 的情况下,那 buffer size 就分配的和接收值数量一样就可以解决这样的问题
  • 4、select操作在所有case上阻塞
    实现一个 fibonacci 数列生成器,并在独立的 goroutine 中运行,在读取完需要长度的数列后,如果 用于 退出生成器的 quit 忘了被 close (或写入数据),select 将一直被阻塞造成 该 goroutine 泄露
func fibonacci(c, quit chan int)  {
    x, y := 0, 1
    for{
        select {
        case c <- x:
            x, y = y, x+y
        case <-quit:
            fmt.Println("quit")
            return
        }
    }
}

func main() {
    c := make(chan int)
    quit := make(chan int)

    go fibonacci(c, quit)

    for i := 0; i < 10; i++{
        fmt.Println(<- c)
    }
    
  // close(quit)
}

在这种需要一个独立的 goroutine 作为生成器的场景下,为了能在外部结束这个 goroutine,我们通常有两种方法:

1、使用上述实现里的模式,传入一个 quit channel,配合 select,当不需要的时候,close 这个 quit channel,该 goroutine 就可以退出。
2、使用 context 包

func fibonacci(c chan int, ctx context.Context)  {
    x, y := 0, 1
    for{
        select {
        case c <- x:
            x, y = y, x+y
        case <-ctx.Done():
            fmt.Println("quit")
            return
        }
    }
}

func main() {
    ctx, cancel := context.WithCancel(context.Background())
    defer cancel()

    c := make(chan int)

    go fibonacci(c, ctx)

    for i := 0; i < 10; i++{
        fmt.Println(<- c)
    }

    cancel()

    time.Sleep(5 * time.Second)
}
  • 5、 goroutine进入死循环中,导致资源一直无法释放
goroutine 泄露的防范
  • 创建goroutine时就要想好该goroutine该如何结束
  • 使用chan时,要考虑到 chan阻塞时协程可能的行为
  • 实现循环语句时注意循环的退出条件,避免死循环
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值