Грешка или функция? Събиране на отпадъци, свързано с „обхват“ и „канал“ в Golang

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
Ключовата дума за диапазон не е специална по този начин, просто направените оптимизации на компилатора могат да определят, че s няма да се използва отново. Направете s глобален и той ще блокира.   -  person JimB    schedule 07.09.2017
comment
Само в случай, че не просто експериментирате, а вместо това се опитвате да внедрите деструктори, трябва да ви предупредя - моля, недейте! Идиоматични потребителски типове Go трябва да предоставят изрични методи за освобождаване/освобождаване на ресурси, като Close() за типове, които обвиват FD и сокети. Вижте също.   -  person kostix    schedule 08.09.2017


Отговори (1)


Не съм 100% сигурен дали това се случва, но от runtime godoc, втори до последен абзац за SetFinalizer:

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

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

Никога не съм мислил за тази възможност, но технически GC би могъл да разбере дали може да събере променлива много преди обхватът на променливата да изчезне. Помислете за този опростен пример.

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

Няма причина str да не може да бъде събрано от GC. Никога не се използва отново и е много възможно да знае това.

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