Баг или фича? Сборка мусора, связанная с «диапазоном» и «каналом» в Голанге

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()
}

Выход (перейти 1.8.3):

В рабочем, ch = 1

Финализатор


Я ожидаю, что эта программа заблокируется. runtime.GC() не будет собирать s, так как worker() содержит ссылку на s.chs.

Однако он заканчивается с go 1.8.3. В финализаторе s успешно вызывается даже close(s.chs).

Интересно, имеет ли это какое-то особое отношение к range и GC.

Спасибо большое.


person Bef0rewind    schedule 07.09.2017    source источник
comment
Ключевое слово range не является особенным в этом смысле, просто сделанные оптимизации компилятора могут определить, что s больше не будет использоваться. Сделайте s глобальным, и он заблокируется.   -  person JimB    schedule 07.09.2017
comment
На тот случай, если вы не просто экспериментируете, а вместо этого пытаетесь реализовать деструкторы, я должен вас предупредить — пожалуйста, не надо! Идиоматические пользовательские типы Go должны предоставлять явные методы освобождения/освобождения ресурсов, такие как Close() для типов, которые обертывают FD и сокеты. См. также.   -  person kostix    schedule 08.09.2017


Ответы (1)


Я не уверен на 100%, что это то, что происходит, но из runtime годока предпоследний абзац для SetFinalizer:

Например, если p указывает на структуру, содержащую файловый дескриптор d, и p имеет финализатор, который закрывает этот файловый дескриптор, и если последнее использование p в функции является вызовом syscall.Write(p.d, buf, size ), то p может быть недоступен, как только программа войдет в syscall.Write. Финализатор может запуститься в этот момент, закрыв p.d, что приведет к сбою syscall.Write, потому что он записывает в закрытый файловый дескриптор (или, что еще хуже, в совершенно другой файловый дескриптор, открытый другой горутиной). Чтобы избежать этой проблемы, вызовите runtime.KeepAlive(p) после вызова syscall.Write.

Это наводит меня на мысль, что Finalizer может запускаться уже сразу после последнего использования рассматриваемого var.

Я никогда не думал об этой возможности сам, но технически сборщик мусора сможет узнать, сможет ли он собрать переменную, задолго до того, как область действия переменной исчезнет. Рассмотрим этот упрощенный пример.

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

Нет никаких причин, по которым сборщик мусора не мог бы собрать str. Его больше никогда не используют, и вполне возможно, что он знает об этом.

person RayfenWindspear    schedule 07.09.2017
comment
да, серверная часть компилятора SSA делает так, что переменные могут выйти из области видимости почти сразу после их использования, поскольку гораздо проще определить, когда что-то больше не используется. Я гарантирую, что этот пример не будет работать с более старым компилятором, таким как go1.6. - person JimB; 07.09.2017
comment
@JimB Да, этот пример не работает с go 1.7. - person Bef0rewind; 07.09.2017