Bug还是特性?Golang中与’range’和’channel’相关的垃圾回收问题

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

Bug or Feature? Garbage Collection related to 'range' and 'channel' in Golang

问题

包 main

import (
"sync"
"runtime"
)

type S struct {
chs chan int
}

var wg sync.WaitGroup

func worker(s *S) {
for i := range s.chs {
println("在 worker 中,ch = ", i)
}

wg.Done()
}

func main() {
s := S{make(chan int)}

runtime.SetFinalizer(&s, func(ss *S) {
println("Finalizer")
close(ss.chs)
})

wg.Add(1)

go worker(&s)
for i := 0; i < 1; i++ {
s.chs <- 1
}

runtime.GC()

wg.Wait()
}

输出结果(go 1.8.3):

在 worker 中,ch = 1

Finalizer


我预计这个程序会发生死锁。runtime.GC() 不会回收 s,因为 worker() 持有对 s.chs 的引用。

然而,它在 go 1.8.3 中终止。在 s 的 finalizer 中,甚至成功调用了 close(s.chs)

我想知道这是否与 range 和 GC 有关。

非常感谢。

英文:
package main

import (
   &quot;sync&quot;
   &quot;runtime&quot;
)

type S struct {
   chs chan int
}

var wg sync.WaitGroup

func worker(s *S) {
   for i := range s.chs {
      println(&quot;In worker, ch = &quot;, i)
   }

   wg.Done()
}

func main() {
   s := S{make(chan int)}

   runtime.SetFinalizer(&amp;s, func(ss *S) {
      println(&quot;Finalizer&quot;)
      close(ss.chs)
   })


   wg.Add(1)

   go worker(&amp;s)
   for i := 0; i &lt; 1; i++ {
      s.chs &lt;- 1
   }

   runtime.GC()

   wg.Wait()
}

Output (go 1.8.3):

> In worker, ch = 1
>
> Finalizer


I expect this program to deadlock. runtime.GC() will not collect s, since worker() holds a reference to s.chs.

However it terminates with go 1.8.3. In the finalizer of s, even close(s.chs) is called successfully.

I wonder if it has something special do with range and GC.

Thanks very much.

答案1

得分: 1

我不100%确定发生了什么,但从runtime的godoc中,关于SetFinalizer的倒数第二段可以看出:

例如,如果p指向一个包含文件描述符d的结构体,并且p有一个关闭该文件描述符的finalizer,如果p在函数中的最后一次使用是对syscall.Write(p.d, buf, size)的调用,那么一旦程序进入syscall.Write,p可能就变得不可达。finalizer可能会在那时运行,关闭p.d,导致syscall.Write失败,因为它正在写入一个关闭的文件描述符(或者更糟糕的是,写入由不同goroutine打开的完全不同的文件描述符)。为了避免这个问题,在调用syscall.Write之后调用runtime.KeepAlive(p)。

这表明Finalizer可能会在相关变量的最后一次使用之后立即运行。

我自己从未考虑过这种可能性,但从技术上讲,GC可以在变量的作用域消失之前就知道是否可以回收该变量。考虑下面这个简单的例子:

func main() {
    str := "Hello World"
    fmt.Println(str)
    someMainLoop()
    // 这之后没有任何代码,但someMainLoop()会一直运行,直到手动停止
}

没有理由GC不能回收str。它再也没有被使用过,而且很可能GC知道这一点。

英文:

I'm not 100% sure if this is what is going on, but from the runtime godoc, second to last paragraph for SetFinalizer:

> For example, if p points to a struct that contains a file descriptor
> d, and p has a finalizer that closes that file descriptor, and if the
> last use of p in a function is a call to syscall.Write(p.d, buf,
> size), then p may be unreachable as soon as the program enters
> syscall.Write. The finalizer may run at that moment, closing p.d,
> causing syscall.Write to fail because it is writing to a closed file
> descriptor (or, worse, to an entirely different file descriptor opened
> by a different goroutine). To avoid this problem, call
> runtime.KeepAlive(p) after the call to syscall.Write.

Which suggests to me that the Finalizer can possibly run as early as immediately after the last usage of the var in question.

I've never thought of this possibility myself, but technically, the GC would be able to know if it could collect a variable long before the variable's scope is gone. Consider this simplistic example.

func main() {
    str := &quot;Hello World&quot;
    fmt.Println(str)
    someMainLoop()
    // nothing after this, but someMainLoop() continues until stopped manually
}

There is no reason why str couldn't be collected by the GC. It is never used again, and it is very possible it knows this.

huangapple
  • 本文由 发表于 2017年9月8日 00:00:19
  • 转载请务必保留本文链接:https://go.coder-hub.com/46100756.html
匿名

发表评论

匿名网友

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

确定