Cơ chế chiếm quyền Goroutine chạy lâu trong trình điều phối Go

Trình điều phối Go sử dụng một luồng giám sát đặc biệt để xử lý việc chiếm quyền các goroutine chạy quá thời gian quy định. Bài viết này sẽ phân tích chi tiết cách thức thực hiện cơ chế này thông qua mã nguồn.

Luồng giám sát sysmon

Trong quá trình khởi tạo chương trình chính, hệ thống sẽ tạo ra một luồng giám sát tên là sysmon. Luồng này hoạt động độc lập trên ngăn xếp hệ thống và không cần liên kết với bất kỳ P nào:

func main() {
    // ... 
    if GOARCH != "wasm" {
        systemstack(func() {
            newm(sysmon, nil, -1)
        })
    }
    // ...
}

Hàm sysmon thực hiện chu kỳ giám sát định kỳ, trong đó gọi hàm retake để kiểm tra và xử lý các trường hợp cần chiếm quyền:

func sysmon() {
    idle := 0
    delay := uint32(0)

    for {
        if idle == 0 {
            delay = 20
        } else if idle > 50 {
            delay *= 2
        }
        if delay > 10*1000 {
            delay = 10 * 1000
        }
        usleep(delay)

        if retake(now) != 0 {
            idle = 0
        } else {
            idle++
        }
    }
}

Xử lý chiếm quyền goroutine chạy lâu

Hàm retake chịu trách nhiệm chính trong việc phát hiện và xử lý các goroutine chạy vượt quá thời gian cho phép:

func retake(now int64) uint32 {
    n := 0
    lock(&allpLock)
    
    for i := 0; i < len(allp); i++ {
        pp := allp[i]
        if pp == nil {
            continue
        }

        pd := &pp.sysmontick
        s := pp.status
        
        if s == _Prunning || s == _Psyscall {
            t := int64(pp.schedtick)
            if int64(pd.schedtick) != t {
                pd.schedtick = uint32(t)
                pd.schedwhen = now
            } else if pd.schedwhen+forcePreemptNS <= now {
                preemptone(pp)
                sysretake = true
            }
        }
    }
    unlock(&allpLock)
    return uint32(n)
}

Khi phát hiện P đang chạy goroutine vượt quá ngưỡng 10ms, hàm preemptone sẽ được gọi để thực hiện chiếm quyền:

func preemptone(pp *p) bool {
    mp := pp.m.ptr()
    if mp == nil || mp == getg().m {
        return false
    }
    
    gp := mp.curg
    if gp == nil || gp == mp.g0 {
        return false
    }

    gp.preempt = true
    gp.stackguard0 = stackPreempt

    if preemptMSupported && debug.asyncpreemptoff == 0 {
        pp.preempt = true
        preemptM(mp)
    }

    return true
}

Cơ chế kiểm tra và chuyển đổi ngữ cảnh

Khi goroutine thực hiện lời gọi hàm tiếp theo, quá trình kiểm tra ngăn xếp sẽ kích hoạt cơ chế chiếm quyền:

TEXT main.gpm(SB)
        cmp rsp, qword ptr [r14+0x10]
        jbe 0x462350
        // ... mã thực thi bình thường
        call $runtime.morestack_noctxt

stackguard0 đã được thiết lập thành giá trị stackPreempt (một số rất lớn), nên lệnh so sánh luôn thất bại và dẫn đến việc gọi hàm mở rộng ngăn xếp morestack_noctxt:

TEXT runtime·morestack_noctxt(SB),NOSPLIT,$0
    MOVL $0, DX
    JMP runtime·morestack(SB)

func newstack() {
    thisg := getg()
    gp := thisg.m.curg
    
    stackguard0 := atomic.Loaduintptr(&gp.stackguard0)
    preempt := stackguard0 == stackPreempt
    
    if preempt {
        if !canPreemptM(thisg.m) {
            gp.stackguard0 = gp.stack.lo + stackGuard
            gogo(&gp.sched)
        }
    }
    
    if preempt {
        gopreempt_m(gp)
    }
}

Cuối cùng, hàm gopreempt_m sẽ thực hiện chuyển đổi ngữ cảnh hoàn chỉnh:

func gopreempt_m(gp *g) {
    goschedImpl(gp)
}

func goschedImpl(gp *g) {
    status := readgstatus(gp)
    casgstatus(gp, _Grunning, _Grunnable)
    dropg()
    
    lock(&sched.lock)
    globrunqput(gp)
    unlock(&sched.lock)

    schedule()
}

Quá trình này chuyển trạng thái goroutine từ đang chạy sang sẵn sàng, giải phóng mối liên kết giữa goroutine và thread, sau đó đưa goroutine vào hàng đợi toàn cục trước khi tiến hành lịch trình mới.

Thẻ: go-runtime Scheduler golang-concurrency goroutine-preemption sysmon-thread

Đăng vào ngày 26 tháng 8 lúc 17:58