返回介绍

上卷 程序设计

中卷 标准库

下卷 运行时

源码剖析

附录

4.5.5 终止线程

发布于 2024-10-12 19:15:59 字数 5803 浏览 0 评论 0 收藏 0

线程自入口函数开始,进入调度循环,便不再返回。哪怕休眠被唤醒,也是回到调度循环内。

// proc.go

// Create a new g running fn.
func newproc(fn *funcval) {
    
    // g0
    systemstack(func() {
        newg := newproc1(fn, gp, pc)        
        _p_ := getg().m.p.ptr()
        runqput(_p_, newg, true)
        
        if mainStarted {
            wakep()
        }
    })
}

func wakep() {
	startm(nil, true)
}
// proc.go

// mstart is the entry-point for new Ms.
// It is written in assembly, uses ABI0, is marked TOPFRAME, and calls mstart0.
func mstart()

// mstart0 is the Go entry-point for new Ms.
func mstart0() {
	mstart1()
	mexit(osStack)
}

func mstart1() {
    
    // g0
	_g_ := getg()
	if _g_ != _g_.m.g0 {
		throw("bad runtime·mstart")
	}

    // 初始化 g0.sched。
	_g_.sched.g = guintptr(unsafe.Pointer(_g_))
	_g_.sched.pc = getcallerpc()
	_g_.sched.sp = getcallersp()

	schedule()
}

入口函数初始化 g0.sched 设置,寄存器值指向 mstart0 。也就是说,内核函数执行时,第一帧是 mstart0。相关函数不会修改 g0.sched 属性,总是自第二帧开始复用。

调度函数 schedule 找到任务 G 后,以 execute 调用 gogo(G.sched) 切换用户栈,执行用户函数。该函数执行结束,其尾部 RET 指令转入 goexit,再次切换回 g0,回到调度函数。

只有 “出错” 时,才会执行 gogo(g0.sched) 。此时 gogo jmp g0.sched.pc ,跳转回 mstart0,执行 mexit 终止线程。

// Finishes execution of the current goroutine.
func goexit1() {
	mcall(goexit0)
}

// goexit continuation on g0.
func goexit0(gp *g) {
    ...
    
    locked := gp.lockedm != 0
	if locked {
		// The goroutine may have locked this thread because
		// it put it in an unusual kernel state. Kill it
		// rather than returning it to the thread pool.

		// Return to mstart, which will release the P and exit
		// the thread.
		if GOOS != "plan9" { // See golang.org/issue/22227.
			gogo(&_g_.m.g0.sched)
		} else {
			// Clear lockedExt on plan9 since we may end up re-using
			// this thread.
			_g_.m.lockedExt = 0
		}
	}
    
	schedule()
}

终止前,从 allm 移除,并释放相关资源。

// mexit tears down and exits the current thread.
//
// Don't call this directly to exit the thread, since it must run at
// the top of the thread stack. Instead, use gogo(&_g_.m.g0.sched) to
// unwind the stack to the point that exits the thread.

func mexit(osStack bool) {
	g := getg()
	m := g.m

	// Free the gsignal stack...

	// Remove m from allm.
	lock(&sched.lock)
	for pprev := &allm; *pprev != nil; pprev = &(*pprev).alllink {
		if *pprev == m {
			*pprev = m.alllink
			goto found
		}
	}
	throw("m not found in allm")
    
found:
    
    // 加入 sched.freem 链表。
	if !osStack {
		atomic.Store(&m.freeWait, 1)
		m.freelink = sched.freem
		sched.freem = m
	}

    // 释放 P 和相关资源。
	handoffp(releasep())
	mdestroy(m)

	if osStack {
		// Return from mstart and let the system thread
		// library free the g0 stack and terminate the thread.
		return
	}

	// mstart is the thread's entry point, so there's nothing to
	// return to. Exit the thread directly. exitThread will clear
	// m.freeWait when it's done with the stack and the m can be
	// reaped.
	exitThread(&m.freeWait)
}
// sys_linux_amd64.s

// func exitThread(wait *uint32)
TEXT runtime·exitThread(SB),NOSPLIT,$0-8
    MOVQ	wait+0(FP), AX
    
    // We're done using the stack.
    MOVL	$0, (AX)
    MOVL	$0, DI	// exit code
    MOVL	$SYS_exit, AX
    SYSCALL
    
    // We may not even have a stack any more.
    INT	$3
	JMP	0(PC)

至于 sched.freem 链表,在 allocm 里被清理,释放所持有的 g0 内存。

// proc.go

// Allocate a new m unassociated with any thread.
func allocm(_p_ *p, fn func(), id int64) *m {
    
    // Release the free M list. We need to do this somewhere and
    // this may free up a stack we can use.
    if sched.freem != nil {
        lock(&sched.lock)
        
        var newList *m
        for freem := sched.freem; freem != nil; {
            if freem.freeWait != 0 {
                next := freem.freelink
                freem.freelink = newList
                newList = freem
                freem = next
                continue
            }
            
            // stackfree must be on the system stack, but allocm is
            // reachable off the system stack transitively from
            // startm.
            systemstack(func() {
                stackfree(freem.g0.stack)
            })
            
            freem = freem.freelink
        }
        
        sched.freem = newList
        unlock(&sched.lock)
    }
}

测试

结论:正常情况,线程不退出,进入休眠队列。当前只有 UnlockOSThread 出错,才导致终止。

// test/main.go

package main

import (
	"runtime"
	"time"
)


func test() {
	runtime.LockOSThread()
	// defer runtime.UnlockOSThread()

	time.Sleep(time.Second * 3)
}

func main() {
	for i := 0; i < 100; i++ {
		go test()
	}

	time.Sleep(time.Second * 5)
}

按是否调用 UnlockOSThread 查看输出结果。线程数量很好地说明线程休眠,或是被终止。

$ go build && GODEBUG=schedtrace=500 ./test   ; 解锁。
SCHED    0ms: threads=4   idlethreads=1
SCHED  504ms: threads=104 idlethreads=1
SCHED 1004ms: threads=104 idlethreads=1
SCHED 1506ms: threads=104 idlethreads=1
SCHED 2007ms: threads=104 idlethreads=1
SCHED 2515ms: threads=104 idlethreads=1
SCHED 3023ms: threads=104 idlethreads=101
SCHED 3526ms: threads=104 idlethreads=101
SCHED 4033ms: threads=104 idlethreads=101
SCHED 4544ms: threads=104 idlethreads=101

$ go build && GODEBUG=schedtrace=500 ./test  ; 未解锁。
SCHED    0ms: threads=7   idlethreads=0
SCHED  504ms: threads=103 idlethreads=0
SCHED 1013ms: threads=103 idlethreads=0
SCHED 1515ms: threads=103 idlethreads=0
SCHED 2015ms: threads=103 idlethreads=0
SCHED 2527ms: threads=103 idlethreads=0
SCHED 3032ms: threads=7   idlethreads=4
SCHED 3538ms: threads=7   idlethreads=4
SCHED 4049ms: threads=7   idlethreads=4
SCHED 4561ms: threads=7   idlethreads=4

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。
列表为空,暂无数据
    我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
    原文