Skip to content

Commit a5a6f61

Browse files
committed
runtime: fix (*gcSweepBuf).block guarantees
Currently gcSweepBuf guarantees that push operations may be performed concurrently with each other and that block operations may be performed concurrently with push operations as well. Unfortunately, this isn't quite true. The existing code allows push operations to happen concurrently with each other, but block operations may return blocks with nil entries. The way this can happen is if two concurrent pushers grab a slot to push to, and the first one (the one with the earlier slot in the buffer) doesn't quite write a span value when the block is called. The existing code in block only checks if the very last value in the block is nil, when really an arbitrary number of the last few values in the block may or may not be nil. Today, this case can't actually happen because when push operations happen concurrently during a GC (which is the only time block is called), they only ever happen during an allocation with the heap lock held, effectively serializing them. A block operation may happen concurrently with one of these pushes, but its callers will never see a nil mspan. Outside of a GC, this isn't a problem because although push operations from allocations can run concurrently with push operations from sweeping, block operations will never run. In essence, the real concurrency guarantees provided by gcSweepBuf are that block operations may happen concurrently with push operations, but that push operations may not be concurrent with each other if there are any block operations. To fix this, and to prepare for push operations happening without the heap lock held in a future CL, we update the documentation for block to correctly state that there may be nil entries in the returned slice. While we're here, make the mspan writes into the buffer atomic to avoid a block user racing on a nil check, and document that the user should load mspan values from the returned slice atomically. Finally, we make all callers of block adhere to the new rules. We choose to allow nil values rather than filter them out because the only caller of block is markrootSpans, and if it catches a nil entry, then there wasn't anything to mark in there anyway since the span is just being created. Updates #35112. Change-Id: I6450aab15f51690d7a000ba5b3d529cf2ca5da1e Reviewed-on: https://go-review.googlesource.com/c/go/+/203318 Run-TryBot: Michael Knyszek <[email protected]> TryBot-Result: Gobot Gobot <[email protected]> Reviewed-by: Austin Clements <[email protected]>
1 parent dac936a commit a5a6f61

File tree

2 files changed

+19
-16
lines changed

2 files changed

+19
-16
lines changed

src/runtime/mgcmark.go

Lines changed: 13 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -315,15 +315,21 @@ func markrootSpans(gcw *gcWork, shard int) {
315315
sg := mheap_.sweepgen
316316
spans := mheap_.sweepSpans[mheap_.sweepgen/2%2].block(shard)
317317
// Note that work.spans may not include spans that were
318-
// allocated between entering the scan phase and now. This is
319-
// okay because any objects with finalizers in those spans
320-
// must have been allocated and given finalizers after we
321-
// entered the scan phase, so addfinalizer will have ensured
322-
// the above invariants for them.
323-
for _, s := range spans {
318+
// allocated between entering the scan phase and now. We may
319+
// also race with spans being added into sweepSpans when they're
320+
// just created, and as a result we may see nil pointers in the
321+
// spans slice. This is okay because any objects with finalizers
322+
// in those spans must have been allocated and given finalizers
323+
// after we entered the scan phase, so addfinalizer will have
324+
// ensured the above invariants for them.
325+
for i := 0; i < len(spans); i++ {
326+
// sweepBuf.block requires that we read pointers from the block atomically.
327+
// It also requires that we ignore nil pointers.
328+
s := (*mspan)(atomic.Loadp(unsafe.Pointer(&spans[i])))
329+
324330
// This is racing with spans being initialized, so
325331
// check the state carefully.
326-
if s.state.get() != mSpanInUse {
332+
if s == nil || s.state.get() != mSpanInUse {
327333
continue
328334
}
329335
// Check that this span was swept (it may be cached or uncached).

src/runtime/mgcsweepbuf.go

Lines changed: 6 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -111,8 +111,9 @@ retry:
111111
unlock(&b.spineLock)
112112
}
113113

114-
// We have a block. Insert the span.
115-
block.spans[bottom] = s
114+
// We have a block. Insert the span atomically, since there may be
115+
// concurrent readers via the block API.
116+
atomic.StorepNoWB(unsafe.Pointer(&block.spans[bottom]), unsafe.Pointer(s))
116117
}
117118

118119
// pop removes and returns a span from buffer b, or nil if b is empty.
@@ -147,7 +148,9 @@ func (b *gcSweepBuf) numBlocks() int {
147148
}
148149

149150
// block returns the spans in the i'th block of buffer b. block is
150-
// safe to call concurrently with push.
151+
// safe to call concurrently with push. The block may contain nil
152+
// pointers that must be ignored, and each entry in the block must be
153+
// loaded atomically.
151154
func (b *gcSweepBuf) block(i int) []*mspan {
152155
// Perform bounds check before loading spine address since
153156
// push ensures the allocated length is at least spineLen.
@@ -169,11 +172,5 @@ func (b *gcSweepBuf) block(i int) []*mspan {
169172
} else {
170173
spans = block.spans[:bottom]
171174
}
172-
173-
// push may have reserved a slot but not filled it yet, so
174-
// trim away unused entries.
175-
for len(spans) > 0 && spans[len(spans)-1] == nil {
176-
spans = spans[:len(spans)-1]
177-
}
178175
return spans
179176
}

0 commit comments

Comments
 (0)