Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

sync: Once keeps the function to be executed alive #55854

Closed
CAFxX opened this issue Sep 25, 2022 · 5 comments
Closed

sync: Once keeps the function to be executed alive #55854

CAFxX opened this issue Sep 25, 2022 · 5 comments
Labels
compiler/runtime Issues related to the Go compiler and/or runtime. FrozenDueToAge NeedsInvestigation Someone must examine and confirm this is a valid issue and not a duplicate of an existing one.
Milestone

Comments

@CAFxX
Copy link
Contributor

CAFxX commented Sep 25, 2022

The idiomatic usage of Once.Do keeps the function to be executed once (and everything said function transitively references) alive even after the first call to Do has completed.

As an example, on go tip this never completes, as even if the runtime.KeepAlive can never be reached after the first iteration, the compiler/runtime is not able to notice this fact:

func TestOnceGC(t *testing.T) {
	var collected atomic.Bool
	something := new([1024]byte)
	runtime.SetFinalizer(something, func(_ any) {
		collected.Store(true)
	})

	var once sync.Once
	for !collected.Load() {
		once.Do(func() {
			runtime.KeepAlive(something)
		})
		runtime.GC()
	}
}

This is clearly not a problem specific to sync.Once (as it can happen in many other cases, including cases not involving function calls at all) but it is particularly visible in this case given the semantics and common use cases of sync.Once.

I am not sure whether this is something that needs fixing, and if so whether it should be fixed in sync or in the compiler/runtime, or if it is just something we should document.

@gopherbot gopherbot added the compiler/runtime Issues related to the Go compiler and/or runtime. label Sep 25, 2022
@ianlancetaylor
Copy link
Contributor

I don't see any way to change this. Any arbitrary call to the Do method may invoke the function. There is no way to know that the function is no longer needed, so it must be live.

@CAFxX
Copy link
Contributor Author

CAFxX commented Sep 25, 2022

FWIW I also don't see simple ways to change this without API changes. Maybe the compiler could notice that once o.load becomes non-0 it can't ever be 0 again, and once that happens start considering f, and the capture itself, dead (this seems quite difficult to implement though).

Or we could maybe document that if this is a concern, people should manually nil out references (as already done in e.g. some places in the standard library). Adding a something = nil after the runtime.KeepAlive in the example above as expected causes the test to complete immediately.

@bcmills
Copy link
Contributor

bcmills commented Sep 26, 2022

Is it really even worth documenting as a concern? It seems intuitive to me that the argument to once.Do is live during the call to Do, even if that call doesn't end up executing that argument, because that is the case for essentially every method call in the language already.¹

That is: I think that if you reason about once.Do as an ordinary method call, the concern about reachability is exactly the same as for the argument to any other ordinary method call in a Go program.

¹ With the possible exception of methods that are trivial to inline and unconditionally ignore their arguments.

@dmitshur dmitshur added the NeedsInvestigation Someone must examine and confirm this is a valid issue and not a duplicate of an existing one. label Sep 26, 2022
@dmitshur dmitshur added this to the Backlog milestone Sep 26, 2022
@dsnet
Copy link
Member

dsnet commented Sep 26, 2022

The current behavior is what I would expect from the current API of sync.Once. If I'm passing a function to sync.Once.Do, then I would expect transitive dependencies of that function to be kept alive even if I mentally knew that it wouldn't be executed.

One way to solve this would be to change the API of sync.Once to accept a function to call, and for it to automatically clear the function once it has been called:

// NewOnceV2 returns a new OnceV2 where f
// is called upon the first call to OnceV2.Do.
func NewOnceV2(f func()) *OnceV2

// OnceV2.Do executes the function passed to NewOnceV2 at most once.
func (*OnceV2) Do() // internal implementation drops reference to f after first call

While the API could allow the function to be garbage collected, I think it has worse properties.

In particular, the compiler may not be able to prove that f doesn't escape and has to heap allocate it. The API today has the benefit that f does not escape when calling sync.Once.Do so that I can pass locally defined closures without worrying about heap allocations.

@mknyszek
Copy link
Contributor

It seems to me like there may be nothing we can do about this. Closing this for now, but feel free to reopen if that changes!

@golang golang locked and limited conversation to collaborators Sep 28, 2023
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
compiler/runtime Issues related to the Go compiler and/or runtime. FrozenDueToAge NeedsInvestigation Someone must examine and confirm this is a valid issue and not a duplicate of an existing one.
Projects
None yet
Development

No branches or pull requests

7 participants