Skip to content

Commit eff2b26

Browse files
committed
runtime: make it possible to exit Go-created threads
Currently, threads created by the runtime exist until the whole program exits. For #14592 and #20395, we want to be able to exit and clean up threads created by the runtime. This commit implements that mechanism. The main difficulty is how to clean up the g0 stack. In cgo mode and on Solaris and Windows where the OS manages thread stacks, we simply arrange to return from mstart and let the system clean up the thread. If the runtime allocated the g0 stack, then we use a new exitThread syscall wrapper that arranges to clear a flag in the M once the stack can safely be reaped and call the thread termination syscall. exitThread is based on the existing exit1 wrapper, which was always meant to terminate the calling thread. However, exit1 has never been used since it was introduced 9 years ago, so it was broken on several platforms. exitThread also has the additional complication of having to flag that the stack is unused, which requires some tricks on platforms that use the stack for syscalls. This still leaves the problem of how to reap the unused g0 stacks. For this, we move the M from allm to a new freem list as part of the M exiting. Later, allocm scans the freem list, finds Ms that are marked as done with their stack, removes these from the list and frees their g0 stacks. This also allows these Ms to be garbage collected. This CL does not yet use any of this functionality. Follow-up CLs will. Likewise, there are no new tests in this CL because we'll need follow-up functionality to test it. Change-Id: Ic851ee74227b6d39c6fc1219fc71b45d3004bc63 Reviewed-on: https://go-review.googlesource.com/46037 Run-TryBot: Austin Clements <[email protected]> TryBot-Result: Gobot Gobot <[email protected]> Reviewed-by: Keith Randall <[email protected]>
1 parent a9c3d09 commit eff2b26

38 files changed

+476
-102
lines changed

src/runtime/cgo/gcc_libinit.c

+4
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,10 @@ _cgo_try_pthread_create(pthread_t* thread, const pthread_attr_t* attr, void* (*p
9898

9999
for (tries = 0; tries < 20; tries++) {
100100
err = pthread_create(thread, attr, pfn, arg);
101+
if (err == 0) {
102+
pthread_detach(*thread);
103+
return 0;
104+
}
101105
if (err != EAGAIN) {
102106
return err;
103107
}

src/runtime/os3_solaris.go

+6
Original file line numberDiff line numberDiff line change
@@ -181,6 +181,12 @@ func newosproc(mp *m, _ unsafe.Pointer) {
181181
}
182182
}
183183

184+
func exitThread(wait *uint32) {
185+
// We should never reach exitThread on Solaris because we let
186+
// libc clean up threads.
187+
throw("exitThread")
188+
}
189+
184190
var urandom_dev = []byte("/dev/urandom\x00")
185191

186192
//go:nosplit

src/runtime/os_nacl.go

+3
Original file line numberDiff line numberDiff line change
@@ -168,6 +168,9 @@ func newosproc(mp *m, stk unsafe.Pointer) {
168168
}
169169
}
170170

171+
//go:noescape
172+
func exitThread(wait *uint32)
173+
171174
//go:nosplit
172175
func semacreate(mp *m) {
173176
if mp.waitsema != 0 {

src/runtime/os_netbsd.go

+4-1
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,9 @@ const (
2121
_UC_SIGMASK = 0x01
2222
_UC_CPU = 0x04
2323

24+
// From <sys/lwp.h>
25+
_LWP_DETACHED = 0x00000040
26+
2427
_EAGAIN = 35
2528
)
2629

@@ -182,7 +185,7 @@ func newosproc(mp *m, stk unsafe.Pointer) {
182185

183186
lwp_mcontext_init(&uc.uc_mcontext, stk, mp, mp.g0, funcPC(netbsdMstart))
184187

185-
ret := lwp_create(unsafe.Pointer(&uc), 0, unsafe.Pointer(&mp.procid))
188+
ret := lwp_create(unsafe.Pointer(&uc), _LWP_DETACHED, unsafe.Pointer(&mp.procid))
186189
sigprocmask(_SIG_SETMASK, &oset, nil)
187190
if ret < 0 {
188191
print("runtime: failed to create new OS thread (have ", mcount()-1, " already; errno=", -ret, ")\n")

src/runtime/os_plan9.go

+6
Original file line numberDiff line numberDiff line change
@@ -421,6 +421,12 @@ func newosproc(mp *m, stk unsafe.Pointer) {
421421
}
422422
}
423423

424+
func exitThread(wait *uint32) {
425+
// We should never reach exitThread on Plan 9 because we let
426+
// the OS clean up threads.
427+
throw("exitThread")
428+
}
429+
424430
//go:nosplit
425431
func semacreate(mp *m) {
426432
}

src/runtime/os_windows.go

+9
Original file line numberDiff line numberDiff line change
@@ -640,6 +640,9 @@ func newosproc(mp *m, stk unsafe.Pointer) {
640640
print("runtime: failed to create new OS thread (have ", mcount(), " already; errno=", getlasterror(), ")\n")
641641
throw("runtime.newosproc")
642642
}
643+
644+
// Close thandle to avoid leaking the thread object if it exits.
645+
stdcall1(_CloseHandle, thandle)
643646
}
644647

645648
// Used by the C library build mode. On Linux this function would allocate a
@@ -651,6 +654,12 @@ func newosproc0(mp *m, stk unsafe.Pointer) {
651654
newosproc(mp, stk)
652655
}
653656

657+
func exitThread(wait *uint32) {
658+
// We should never reach exitThread on Windows because we let
659+
// the OS clean up threads.
660+
throw("exitThread")
661+
}
662+
654663
// Called to initialize a new m (including the bootstrap m).
655664
// Called on the parent thread (main thread in case of bootstrap), can allocate memory.
656665
func mpreinit(mp *m) {

src/runtime/proc.go

+134-9
Original file line numberDiff line numberDiff line change
@@ -1152,7 +1152,8 @@ func startTheWorldWithSema(emitTraceEvent bool) int64 {
11521152
func mstart() {
11531153
_g_ := getg()
11541154

1155-
if _g_.stack.lo == 0 {
1155+
osStack := _g_.stack.lo == 0
1156+
if osStack {
11561157
// Initialize stack bounds from system stack.
11571158
// Cgo may have left stack size in stack.hi.
11581159
size := _g_.stack.hi
@@ -1166,21 +1167,30 @@ func mstart() {
11661167
// both Go and C functions with stack growth prologues.
11671168
_g_.stackguard0 = _g_.stack.lo + _StackGuard
11681169
_g_.stackguard1 = _g_.stackguard0
1169-
mstart1()
1170+
mstart1(0)
1171+
1172+
// Exit this thread.
1173+
if GOOS == "windows" || GOOS == "solaris" {
1174+
// Windows and Solaris always system-allocate the
1175+
// stack, but put it in _g_.stack before mstart, so
1176+
// the logic above hasn't set osStack yet.
1177+
osStack = true
1178+
}
1179+
mexit(osStack)
11701180
}
11711181

1172-
func mstart1() {
1182+
func mstart1(dummy int32) {
11731183
_g_ := getg()
11741184

11751185
if _g_ != _g_.m.g0 {
11761186
throw("bad runtime·mstart")
11771187
}
11781188

1179-
// Record top of stack for use by mcall.
1180-
// Once we call schedule we're never coming back,
1181-
// so other calls can reuse this stack space.
1182-
gosave(&_g_.m.g0.sched)
1183-
_g_.m.g0.sched.pc = ^uintptr(0) // make sure it is never used
1189+
// Record the caller for use as the top of stack in mcall and
1190+
// for terminating the thread.
1191+
// We're never coming back to mstart1 after we call schedule,
1192+
// so other calls can reuse the current frame.
1193+
save(getcallerpc(), getcallersp(unsafe.Pointer(&dummy)))
11841194
asminit()
11851195
minit()
11861196

@@ -1219,6 +1229,99 @@ func mstartm0() {
12191229
initsig(false)
12201230
}
12211231

1232+
// mexit tears down and exits the current thread.
1233+
//
1234+
// Don't call this directly to exit the thread, since it must run at
1235+
// the top of the thread stack. Instead, use gogo(&_g_.m.g0.sched) to
1236+
// unwind the stack to the point that exits the thread.
1237+
//
1238+
// It is entered with m.p != nil, so write barriers are allowed. It
1239+
// will release the P before exiting.
1240+
//
1241+
//go:yeswritebarrierrec
1242+
func mexit(osStack bool) {
1243+
g := getg()
1244+
m := g.m
1245+
1246+
if m == &m0 {
1247+
// This is the main thread. Just wedge it.
1248+
//
1249+
// On Linux, exiting the main thread puts the process
1250+
// into a non-waitable zombie state. On Plan 9,
1251+
// exiting the main thread unblocks wait even though
1252+
// other threads are still running. On Solaris we can
1253+
// neither exitThread nor return from mstart. Other
1254+
// bad things probably happen on other platforms.
1255+
//
1256+
// We could try to clean up this M more before wedging
1257+
// it, but that complicates signal handling.
1258+
handoffp(releasep())
1259+
lock(&sched.lock)
1260+
sched.nmfreed++
1261+
checkdead()
1262+
unlock(&sched.lock)
1263+
notesleep(&m.park)
1264+
throw("locked m0 woke up")
1265+
}
1266+
1267+
sigblock()
1268+
unminit()
1269+
1270+
// Free the gsignal stack.
1271+
if m.gsignal != nil {
1272+
stackfree(m.gsignal.stack)
1273+
}
1274+
1275+
// Remove m from allm.
1276+
lock(&sched.lock)
1277+
for pprev := &allm; *pprev != nil; pprev = &(*pprev).alllink {
1278+
if *pprev == m {
1279+
*pprev = m.alllink
1280+
goto found
1281+
}
1282+
}
1283+
throw("m not found in allm")
1284+
found:
1285+
if !osStack {
1286+
// Delay reaping m until it's done with the stack.
1287+
//
1288+
// If this is using an OS stack, the OS will free it
1289+
// so there's no need for reaping.
1290+
atomic.Store(&m.freeWait, 1)
1291+
// Put m on the free list, though it will not be reaped until
1292+
// freeWait is 0. Note that the free list must not be linked
1293+
// through alllink because some functions walk allm without
1294+
// locking, so may be using alllink.
1295+
m.freelink = sched.freem
1296+
sched.freem = m
1297+
}
1298+
unlock(&sched.lock)
1299+
1300+
// Release the P.
1301+
handoffp(releasep())
1302+
// After this point we must not have write barriers.
1303+
1304+
// Invoke the deadlock detector. This must happen after
1305+
// handoffp because it may have started a new M to take our
1306+
// P's work.
1307+
lock(&sched.lock)
1308+
sched.nmfreed++
1309+
checkdead()
1310+
unlock(&sched.lock)
1311+
1312+
if osStack {
1313+
// Return from mstart and let the system thread
1314+
// library free the g0 stack and terminate the thread.
1315+
return
1316+
}
1317+
1318+
// mstart is the thread's entry point, so there's nothing to
1319+
// return to. Exit the thread directly. exitThread will clear
1320+
// m.freeWait when it's done with the stack and the m can be
1321+
// reaped.
1322+
exitThread(&m.freeWait)
1323+
}
1324+
12221325
// forEachP calls fn(p) for every P p when p reaches a GC safe point.
12231326
// If a P is currently executing code, this will bring the P to a GC
12241327
// safe point and execute fn on that P. If the P is not executing code
@@ -1364,6 +1467,27 @@ func allocm(_p_ *p, fn func()) *m {
13641467
if _g_.m.p == 0 {
13651468
acquirep(_p_) // temporarily borrow p for mallocs in this function
13661469
}
1470+
1471+
// Release the free M list. We need to do this somewhere and
1472+
// this may free up a stack we can use.
1473+
if sched.freem != nil {
1474+
lock(&sched.lock)
1475+
var newList *m
1476+
for freem := sched.freem; freem != nil; {
1477+
if freem.freeWait != 0 {
1478+
next := freem.freelink
1479+
freem.freelink = newList
1480+
newList = freem
1481+
freem = next
1482+
continue
1483+
}
1484+
stackfree(freem.g0.stack)
1485+
freem = freem.freelink
1486+
}
1487+
sched.freem = newList
1488+
unlock(&sched.lock)
1489+
}
1490+
13671491
mp := new(m)
13681492
mp.mstartfn = fn
13691493
mcommoninit(mp)
@@ -3377,7 +3501,7 @@ func gcount() int32 {
33773501
}
33783502

33793503
func mcount() int32 {
3380-
return int32(sched.mnext)
3504+
return int32(sched.mnext - sched.nmfreed)
33813505
}
33823506

33833507
var prof struct {
@@ -3902,6 +4026,7 @@ func incidlelocked(v int32) {
39024026

39034027
// Check for deadlock situation.
39044028
// The check is based on number of running M's, if 0 -> deadlock.
4029+
// sched.lock must be held.
39054030
func checkdead() {
39064031
// For -buildmode=c-shared or -buildmode=c-archive it's OK if
39074032
// there are no running goroutines. The calling program is

src/runtime/runtime2.go

+26-3
Original file line numberDiff line numberDiff line change
@@ -169,9 +169,13 @@ func efaceOf(ep *interface{}) *eface {
169169
// a word that is completely ignored by the GC than to have one for which
170170
// only a few updates are ignored.
171171
//
172-
// Gs, Ms, and Ps are always reachable via true pointers in the
173-
// allgs, allm, and allp lists or (during allocation before they reach those lists)
172+
// Gs and Ps are always reachable via true pointers in the
173+
// allgs and allp lists or (during allocation before they reach those lists)
174174
// from stack variables.
175+
//
176+
// Ms are always reachable via true pointers either from allm or
177+
// freem. Unlike Gs and Ps we do free Ms, so it's important that
178+
// nothing ever hold an muintptr across a safe point.
175179

176180
// A guintptr holds a goroutine pointer, but typed as a uintptr
177181
// to bypass write barriers. It is used in the Gobuf goroutine state
@@ -221,6 +225,15 @@ func (pp puintptr) ptr() *p { return (*p)(unsafe.Pointer(pp)) }
221225
//go:nosplit
222226
func (pp *puintptr) set(p *p) { *pp = puintptr(unsafe.Pointer(p)) }
223227

228+
// muintptr is a *m that is not tracked by the garbage collector.
229+
//
230+
// Because we do free Ms, there are some additional constrains on
231+
// muintptrs:
232+
//
233+
// 1. Never hold an muintptr locally across a safe point.
234+
//
235+
// 2. Any muintptr in the heap must be owned by the M itself so it can
236+
// ensure it is not in use when the last true *m is released.
224237
type muintptr uintptr
225238

226239
//go:nosplit
@@ -413,7 +426,8 @@ type m struct {
413426
inwb bool // m is executing a write barrier
414427
newSigstack bool // minit on C thread called sigaltstack
415428
printlock int8
416-
incgo bool // m is executing a cgo call
429+
incgo bool // m is executing a cgo call
430+
freeWait uint32 // if == 0, safe to free g0 and delete m (atomic)
417431
fastrand [2]uint32
418432
needextram bool
419433
traceback uint8
@@ -440,6 +454,7 @@ type m struct {
440454
startingtrace bool
441455
syscalltick uint32
442456
thread uintptr // thread handle
457+
freelink *m // on sched.freem
443458

444459
// these are here because they are too large to be on the stack
445460
// of low-level NOSPLIT functions.
@@ -528,12 +543,16 @@ type schedt struct {
528543

529544
lock mutex
530545

546+
// When increasing nmidle, nmidlelocked, nmsys, or nmfreed, be
547+
// sure to call checkdead().
548+
531549
midle muintptr // idle m's waiting for work
532550
nmidle int32 // number of idle m's waiting for work
533551
nmidlelocked int32 // number of locked m's waiting for work
534552
mnext int64 // number of m's that have been created and next M ID
535553
maxmcount int32 // maximum number of m's allowed (or die)
536554
nmsys int32 // number of system m's not counted for deadlock
555+
nmfreed int64 // cumulative number of freed m's
537556

538557
ngsys uint32 // number of system goroutines; updated atomically
539558

@@ -560,6 +579,10 @@ type schedt struct {
560579
deferlock mutex
561580
deferpool [5]*_defer
562581

582+
// freem is the list of m's waiting to be freed when their
583+
// m.exited is set. Linked through m.freelink.
584+
freem *m
585+
563586
gcwaiting uint32 // gc is waiting to run
564587
stopwait int32
565588
stopnote note

src/runtime/stubs.go

-1
Original file line numberDiff line numberDiff line change
@@ -136,7 +136,6 @@ func gosave(buf *gobuf)
136136

137137
//go:noescape
138138
func jmpdefer(fv *funcval, argp uintptr)
139-
func exit1(code int32)
140139
func asminit()
141140
func setg(gg *g)
142141
func breakpoint()

src/runtime/stubs2.go

+6
Original file line numberDiff line numberDiff line change
@@ -25,3 +25,9 @@ func write(fd uintptr, p unsafe.Pointer, n int32) int32
2525
func open(name *byte, mode, perm int32) int32
2626

2727
func madvise(addr unsafe.Pointer, n uintptr, flags int32)
28+
29+
// exitThread terminates the current thread, writing *wait = 0 when
30+
// the stack is safe to reclaim.
31+
//
32+
//go:noescape
33+
func exitThread(wait *uint32)

0 commit comments

Comments
 (0)