停止一个无限写入通道的 goroutine。

huangapple go评论73阅读模式
英文:

Stop a goroutine that writes to a channel indefinitely

问题

我有一个函数,它创建了一个无限循环地向通道中填充数据的goroutine,例如:

func Foo() <-chan int {
  ch := make(chan int) 
  go func() {
    defer close(ch)
    for {
      ch <- 1
    } 
  }() 
  return ch
}

假设我们有一个消费者希望在一段时间后停止:

ch := Foo() 
<-ch
<-ch
// 完成

现在我想要清理goroutine资源,包括通道。我尝试添加一个"done"通道来实现,但是我遇到了死锁问题:

func Foo() (<-chan int, chan<- bool) {
  ch := make(chan int)
  done := make(chan bool)
  go func() {
    defer close(ch)
    for {
      select {
      case <-done:
          return
      default:
          ch <- 1
      }
    } 
  }() 
  return ch, done
}

func main() {
  ch, done := Foo()
  <-ch
  <-ch
  done <- true
  // 这里
}

现在,它似乎可以工作,但这只是因为程序退出了。如果我将// 这里替换为一些IO操作(例如:http.Get("http://google.com")),我会遇到死锁问题(fatal error: all goroutines are asleep - deadlock!)。
我想知道是否有另一种方法来清理由Foo函数创建的goroutine和通道。

英文:

I have a function that creates a goroutine that populates a channel indefinitely, e.g.,:

func Foo() &lt;-chan int {
  ch := make(chan int) 
  go func() {
    defer close(ch)
    for {
      ch &lt;- 1
    } 
  }() 
  return ch
}

And let's say we have a consumer that would like to stop after a while:

ch:=Foo() 
&lt;-ch
&lt;-ch
// done

Now I want to clean the goroutine resource including the channel. I tried to add a "done" channel for that but then I face a deadlock:

func Foo() (&lt;-chan int, chan&lt;- bool) {
  ch := make(chan int)
  done := make(chan bool)
  go func() {
    defer close(ch)
    for {
      select {
      case &lt;-done:
          return
      default:
          ch &lt;- 1
      }
    } 
  }() 
  return ch, done
}

func main() {
  ch, done := Foo()
  &lt;-ch
  &lt;-ch
  done &lt;- true
  // HERE
}

Now, it seems to work, but it is only because the program exits, if I replace // HERE with some io operation (e.g.,: http.Get(&quot;http://google.com&quot;)), I face a deadlock (fatal error: all goroutines are asleep - deadlock!).
I wonder if there is another way to clean up the spawned goroutine and channel created by the Foo function.

答案1

得分: 3

只需在启动的goroutine中将default替换为case

func Foo() (<-chan int, chan<- bool) {
  ch := make(chan int)
  done := make(chan bool)
  go func() {
    defer close(ch)
    for {
      select {
      case <-done:
          return
      case ch <- 1:
      }
    } 
  }() 
  return ch, done
}

原始代码中的default case会发生死锁,原因如下:

  • 启动的goroutine会立即进入default case,因为没有其他并发运行的goroutine写入done通道,而这些goroutine会向ch通道写入数据。然后,goroutine会在ch <- 1行阻塞,直到其他goroutine从ch读取值。
  • 主goroutine从ch读取两次。这导致启动的goroutine执行两次成功的循环。然后它尝试向done写入数据。此时,启动的goroutine可能已经检查了select语句,进入default case,并在ch <- 1行阻塞。因此,主goroutine也会在done <- true行无限期地阻塞。这导致死锁。
英文:

Just replace default with case inside the started goroutine:

func Foo() (&lt;-chan int, chan&lt;- bool) {
  ch := make(chan int)
  done := make(chan bool)
  go func() {
    defer close(ch)
    for {
      select {
      case &lt;-done:
          return
      case ch &lt;- 1:
      }
    } 
  }() 
  return ch, done
}

The original code with the default case deadlocks because of the following reasons:

  • The started goroutines, which writes to ch channel, instantly goes to the default case when there are no other concurrently running goroutines, which write to done channel. Then the goroutines blocks at the ch &lt;- 1 line until other goroutines reads the value from ch.
  • The main goroutine reads twice from ch. This results in two successful execution loops at the started goroutine. Then it tries writing to done. At this time the started goroutine may already check the select statement, fall into the default case and block at ch &lt;- 1 line. So the main goroutine also blocks indefinitely at the done &lt;- true line. This results in deadlock.

huangapple
  • 本文由 发表于 2023年2月27日 08:21:16
  • 转载请务必保留本文链接:https://go.coder-hub.com/75575854.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定