Skip to content

Commit 0531768

Browse files
committed
runtime: implement AddCleanup
This change introduces AddCleanup to the runtime package. AddCleanup attaches a cleanup function to an pointer to an object. The Stop method on Cleanups will be implemented in a followup CL. AddCleanup is intended to be an incremental improvement over SetFinalizer and will result in SetFinalizer being deprecated. For #67535 Change-Id: I99645152e3fdcee85fcf42a4f312c6917e8aecb1 Reviewed-on: https://go-review.googlesource.com/c/go/+/627695 LUCI-TryBot-Result: Go LUCI <[email protected]> Reviewed-by: Michael Knyszek <[email protected]>
1 parent 8ac0a7c commit 0531768

File tree

8 files changed

+360
-16
lines changed

8 files changed

+360
-16
lines changed

api/next/67535.txt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
pkg runtime, func AddCleanup[$0 interface{}, $1 interface{}](*$0, func($1), $1) Cleanup #67535
2+
pkg runtime, method (Cleanup) Stop() #67535
3+
pkg runtime, type Cleanup struct #67535
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
The [AddCleanup] function attaches a function to a pointer. Once the object that
2+
the pointer points to is no longer reachable, the runtime will call the function.
3+
[AddCleanup] is a finalization mechanism similar to [SetFinalizer]. Unlike
4+
[SetFinalizer], it does not resurrect objects while running the cleanup. Multiple
5+
cleanups can be attached to a single object. [AddCleanup] is an improvement over
6+
[SetFinalizer].

src/runtime/mcleanup.go

Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
// Copyright 2024 The Go Authors. All rights reserved.
2+
// Use of this source code is governed by a BSD-style
3+
// license that can be found in the LICENSE file.
4+
5+
package runtime
6+
7+
import (
8+
"internal/abi"
9+
"unsafe"
10+
)
11+
12+
// AddCleanup attaches a cleanup function to ptr. Some time after ptr is no longer
13+
// reachable, the runtime will call cleanup(arg) in a separate goroutine.
14+
//
15+
// If ptr is reachable from cleanup or arg, ptr will never be collected
16+
// and the cleanup will never run. AddCleanup panics if arg is equal to ptr.
17+
//
18+
// The cleanup(arg) call is not always guaranteed to run; in particular it is not
19+
// guaranteed to run before program exit.
20+
//
21+
// Cleanups are not guaranteed to run if the size of T is zero bytes, because
22+
// it may share same address with other zero-size objects in memory. See
23+
// https://go.dev/ref/spec#Size_and_alignment_guarantees.
24+
//
25+
// There is no specified order in which cleanups will run.
26+
//
27+
// A single goroutine runs all cleanup calls for a program, sequentially. If a
28+
// cleanup function must run for a long time, it should create a new goroutine.
29+
//
30+
// If ptr has both a cleanup and a finalizer, the cleanup will only run once
31+
// it has been finalized and becomes unreachable without an associated finalizer.
32+
//
33+
// It is not guaranteed that a cleanup will run for objects allocated
34+
// in initializers for package-level variables. Such objects may be
35+
// linker-allocated, not heap-allocated.
36+
//
37+
// Note that because cleanups may execute arbitrarily far into the future
38+
// after an object is no longer referenced, the runtime is allowed to perform
39+
// a space-saving optimization that batches objects together in a single
40+
// allocation slot. The cleanup for an unreferenced object in such an
41+
// allocation may never run if it always exists in the same batch as a
42+
// referenced object. Typically, this batching only happens for tiny
43+
// (on the order of 16 bytes or less) and pointer-free objects.
44+
func AddCleanup[T, S any](ptr *T, cleanup func(S), arg S) Cleanup {
45+
// Explicitly force ptr to escape to the heap.
46+
ptr = abi.Escape(ptr)
47+
48+
// The pointer to the object must be valid.
49+
if ptr == nil {
50+
throw("runtime.AddCleanup: ptr is nil")
51+
}
52+
usptr := uintptr(unsafe.Pointer(ptr))
53+
54+
// Check that arg is not equal to ptr.
55+
// TODO(67535) this does not cover the case where T and *S are the same
56+
// type and ptr and arg are equal.
57+
if unsafe.Pointer(&arg) == unsafe.Pointer(ptr) {
58+
throw("runtime.AddCleanup: ptr is equal to arg, cleanup will never run")
59+
}
60+
if inUserArenaChunk(usptr) {
61+
// Arena-allocated objects are not eligible for cleanup.
62+
throw("runtime.AddCleanup: ptr is arena-allocated")
63+
}
64+
if debug.sbrk != 0 {
65+
// debug.sbrk never frees memory, so no cleanup will ever run
66+
// (and we don't have the data structures to record them).
67+
// return a noop cleanup.
68+
return Cleanup{}
69+
}
70+
71+
fn := func() {
72+
cleanup(arg)
73+
}
74+
// closure must escape
75+
fv := *(**funcval)(unsafe.Pointer(&fn))
76+
fv = abi.Escape(fv)
77+
78+
// find the containing object
79+
base, _, _ := findObject(usptr, 0, 0)
80+
if base == 0 {
81+
if isGoPointerWithoutSpan(unsafe.Pointer(ptr)) {
82+
return Cleanup{}
83+
}
84+
throw("runtime.AddCleanup: ptr not in allocated block")
85+
}
86+
87+
// ensure we have a finalizer processing goroutine running.
88+
createfing()
89+
90+
addCleanup(unsafe.Pointer(ptr), fv)
91+
return Cleanup{}
92+
}
93+
94+
// Cleanup is a handle to a cleanup call for a specific object.
95+
type Cleanup struct{}
96+
97+
// Stop cancels the cleanup call. Stop will have no effect if the cleanup call
98+
// has already been queued for execution (because ptr became unreachable).
99+
// To guarantee that Stop removes the cleanup function, the caller must ensure
100+
// that the pointer that was passed to AddCleanup is reachable across the call to Stop.
101+
//
102+
// TODO(amedee) needs implementation.
103+
func (c Cleanup) Stop() {}

src/runtime/mcleanup_test.go

Lines changed: 159 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,159 @@
1+
// Copyright 2024 The Go Authors. All rights reserved.
2+
// Use of this source code is governed by a BSD-style
3+
// license that can be found in the LICENSE file.
4+
5+
package runtime_test
6+
7+
import (
8+
"runtime"
9+
"testing"
10+
"unsafe"
11+
)
12+
13+
func TestCleanup(t *testing.T) {
14+
ch := make(chan bool, 1)
15+
done := make(chan bool, 1)
16+
want := 97531
17+
go func() {
18+
// allocate struct with pointer to avoid hitting tinyalloc.
19+
// Otherwise we can't be sure when the allocation will
20+
// be freed.
21+
type T struct {
22+
v int
23+
p unsafe.Pointer
24+
}
25+
v := &new(T).v
26+
*v = 97531
27+
cleanup := func(x int) {
28+
if x != want {
29+
t.Errorf("cleanup %d, want %d", x, want)
30+
}
31+
ch <- true
32+
}
33+
runtime.AddCleanup(v, cleanup, 97531)
34+
v = nil
35+
done <- true
36+
}()
37+
<-done
38+
runtime.GC()
39+
<-ch
40+
}
41+
42+
func TestCleanupMultiple(t *testing.T) {
43+
ch := make(chan bool, 3)
44+
done := make(chan bool, 1)
45+
want := 97531
46+
go func() {
47+
// allocate struct with pointer to avoid hitting tinyalloc.
48+
// Otherwise we can't be sure when the allocation will
49+
// be freed.
50+
type T struct {
51+
v int
52+
p unsafe.Pointer
53+
}
54+
v := &new(T).v
55+
*v = 97531
56+
cleanup := func(x int) {
57+
if x != want {
58+
t.Errorf("cleanup %d, want %d", x, want)
59+
}
60+
ch <- true
61+
}
62+
runtime.AddCleanup(v, cleanup, 97531)
63+
runtime.AddCleanup(v, cleanup, 97531)
64+
runtime.AddCleanup(v, cleanup, 97531)
65+
v = nil
66+
done <- true
67+
}()
68+
<-done
69+
runtime.GC()
70+
<-ch
71+
<-ch
72+
<-ch
73+
}
74+
75+
func TestCleanupZeroSizedStruct(t *testing.T) {
76+
type Z struct{}
77+
z := new(Z)
78+
runtime.AddCleanup(z, func(s string) {}, "foo")
79+
}
80+
81+
func TestCleanupAfterFinalizer(t *testing.T) {
82+
ch := make(chan int, 2)
83+
done := make(chan bool, 1)
84+
want := 97531
85+
go func() {
86+
// allocate struct with pointer to avoid hitting tinyalloc.
87+
// Otherwise we can't be sure when the allocation will
88+
// be freed.
89+
type T struct {
90+
v int
91+
p unsafe.Pointer
92+
}
93+
v := &new(T).v
94+
*v = 97531
95+
finalizer := func(x *int) {
96+
ch <- 1
97+
}
98+
cleanup := func(x int) {
99+
if x != want {
100+
t.Errorf("cleanup %d, want %d", x, want)
101+
}
102+
ch <- 2
103+
}
104+
runtime.AddCleanup(v, cleanup, 97531)
105+
runtime.SetFinalizer(v, finalizer)
106+
v = nil
107+
done <- true
108+
}()
109+
<-done
110+
runtime.GC()
111+
var result int
112+
result = <-ch
113+
if result != 1 {
114+
t.Errorf("result %d, want 1", result)
115+
}
116+
runtime.GC()
117+
result = <-ch
118+
if result != 2 {
119+
t.Errorf("result %d, want 2", result)
120+
}
121+
}
122+
123+
func TestCleanupInteriorPointer(t *testing.T) {
124+
ch := make(chan bool, 3)
125+
done := make(chan bool, 1)
126+
want := 97531
127+
go func() {
128+
// Allocate struct with pointer to avoid hitting tinyalloc.
129+
// Otherwise we can't be sure when the allocation will
130+
// be freed.
131+
type T struct {
132+
p unsafe.Pointer
133+
i int
134+
a int
135+
b int
136+
c int
137+
}
138+
ts := new(T)
139+
ts.a = 97531
140+
ts.b = 97531
141+
ts.c = 97531
142+
cleanup := func(x int) {
143+
if x != want {
144+
t.Errorf("cleanup %d, want %d", x, want)
145+
}
146+
ch <- true
147+
}
148+
runtime.AddCleanup(&ts.a, cleanup, 97531)
149+
runtime.AddCleanup(&ts.b, cleanup, 97531)
150+
runtime.AddCleanup(&ts.c, cleanup, 97531)
151+
ts = nil
152+
done <- true
153+
}()
154+
<-done
155+
runtime.GC()
156+
<-ch
157+
<-ch
158+
<-ch
159+
}

src/runtime/mfinal.go

Lines changed: 27 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -40,11 +40,14 @@ const (
4040
fingWake
4141
)
4242

43-
var finlock mutex // protects the following variables
44-
var fing *g // goroutine that runs finalizers
45-
var finq *finblock // list of finalizers that are to be executed
46-
var finc *finblock // cache of free blocks
47-
var finptrmask [_FinBlockSize / goarch.PtrSize / 8]byte
43+
// This runs durring the GC sweep phase. Heap memory can't be allocated while sweep is running.
44+
var (
45+
finlock mutex // protects the following variables
46+
fing *g // goroutine that runs finalizers
47+
finq *finblock // list of finalizers that are to be executed
48+
finc *finblock // cache of free blocks
49+
finptrmask [_FinBlockSize / goarch.PtrSize / 8]byte
50+
)
4851

4952
var allfin *finblock // list of all blocks
5053

@@ -172,7 +175,7 @@ func finalizercommit(gp *g, lock unsafe.Pointer) bool {
172175
return true
173176
}
174177

175-
// This is the goroutine that runs all of the finalizers.
178+
// This is the goroutine that runs all of the finalizers and cleanups.
176179
func runfinq() {
177180
var (
178181
frame unsafe.Pointer
@@ -202,6 +205,22 @@ func runfinq() {
202205
for i := fb.cnt; i > 0; i-- {
203206
f := &fb.fin[i-1]
204207

208+
// arg will only be nil when a cleanup has been queued.
209+
if f.arg == nil {
210+
var cleanup func()
211+
fn := unsafe.Pointer(f.fn)
212+
cleanup = *(*func())(unsafe.Pointer(&fn))
213+
fingStatus.Or(fingRunningFinalizer)
214+
cleanup()
215+
fingStatus.And(^fingRunningFinalizer)
216+
217+
f.fn = nil
218+
f.arg = nil
219+
f.ot = nil
220+
atomic.Store(&fb.cnt, i-1)
221+
continue
222+
}
223+
205224
var regs abi.RegArgs
206225
// The args may be passed in registers or on stack. Even for
207226
// the register case, we still need the spill slots.
@@ -220,7 +239,8 @@ func runfinq() {
220239
frame = mallocgc(framesz, nil, true)
221240
framecap = framesz
222241
}
223-
242+
// cleanups also have a nil fint. Cleanups should have been processed before
243+
// reaching this point.
224244
if f.fint == nil {
225245
throw("missing type in runfinq")
226246
}

src/runtime/mgc.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1908,7 +1908,7 @@ func gcTestIsReachable(ptrs ...unsafe.Pointer) (mask uint64) {
19081908
s := (*specialReachable)(mheap_.specialReachableAlloc.alloc())
19091909
unlock(&mheap_.speciallock)
19101910
s.special.kind = _KindSpecialReachable
1911-
if !addspecial(p, &s.special) {
1911+
if !addspecial(p, &s.special, false) {
19121912
throw("already have a reachable special (duplicate pointer?)")
19131913
}
19141914
specials[i] = s

src/runtime/mgcmark.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -178,6 +178,8 @@ func markroot(gcw *gcWork, i uint32, flushBgCredit bool) int64 {
178178
case i == fixedRootFinalizers:
179179
for fb := allfin; fb != nil; fb = fb.alllink {
180180
cnt := uintptr(atomic.Load(&fb.cnt))
181+
// Finalizers that contain cleanups only have fn set. None of the other
182+
// fields are necessary.
181183
scanblock(uintptr(unsafe.Pointer(&fb.fin[0])), cnt*unsafe.Sizeof(fb.fin[0]), &finptrmask[0], gcw, nil)
182184
}
183185

@@ -401,6 +403,10 @@ func markrootSpans(gcw *gcWork, shard int) {
401403
// The special itself is a root.
402404
spw := (*specialWeakHandle)(unsafe.Pointer(sp))
403405
scanblock(uintptr(unsafe.Pointer(&spw.handle)), goarch.PtrSize, &oneptrmask[0], gcw, nil)
406+
case _KindSpecialCleanup:
407+
spc := (*specialCleanup)(unsafe.Pointer(sp))
408+
// The special itself is a root.
409+
scanblock(uintptr(unsafe.Pointer(&spc.fn)), goarch.PtrSize, &oneptrmask[0], gcw, nil)
404410
}
405411
}
406412
unlock(&s.speciallock)

0 commit comments

Comments
 (0)