1
package main

import (
   "sync"
   "runtime"
)

type S struct {
   chs chan int
}

var wg sync.WaitGroup

func worker(s *S) {
   for i := range s.chs {
      println("In 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()
}

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.

Ari Seyhun
  • 11,506
  • 16
  • 62
  • 109
Bef0rewind
  • 26
  • 3
  • The range keyword isn't special in that way, it's just that the compiler optimizations made can determine that `s` will not be used again. Make `s` global and it will deadlock. – JimB Sep 07 '17 at 16:25
  • Just in case you're not merely experimenting but are instead trying to implement "destructors", I have to warn you—please don't! Idiomatic Go custom types should provide explicit "resource deallocation/freeing" methods, such as `Close()` on types which wrap FDs and sockets. [See also](https://stackoverflow.com/q/32768243/720999). – kostix Sep 08 '17 at 11:05

1 Answers1

1

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 := "Hello World"
    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.

RayfenWindspear
  • 6,116
  • 1
  • 30
  • 42
  • 2
    yes, the SSA compiler backend makes it so variables can "go out of scope" almost immediately after their use, since it's much easier to determine when something is no longer used. I guarantee this example will fail with an older compiler like go1.6. – JimB Sep 07 '17 at 16:23
  • @JimB Yes, this example will fail with go 1.7. – Bef0rewind Sep 07 '17 at 16:29