天天看點

2021-03-31 go語言的goroutine排程

  1. go語言的排程設計到幾個重要是資料結構,結構體g,結構體m,結構體p以及sched結構體,

結構體G

struct G
{
    uintptr    stackguard;    // 分段棧的可用空間下界
    uintptr    stackbase;    // 分段棧的棧基址
    Gobuf    sched;        //程序切換時,利用sched域來儲存上下文
    uintptr    stack0;
    FuncVal*    fnstart;        // goroutine運作的函數
    void*    param;        // 用于傳遞參數,睡眠時其它goroutine設定param,喚醒時此goroutine可以擷取
    int16    status;        // 狀态Gidle,Grunnable,Grunning,Gsyscall,Gwaiting,Gdead
    int64    goid;        // goroutine的id号
    G*    schedlink;
    M*    m;        // for debuggers, but offset not hard-coded
    M*    lockedm;    // G被鎖定隻能在這個m上運作
    uintptr    gopc;    // 建立這個goroutine的go表達式的pc
    ...
};
           

     G是goroutine的縮寫,是goroutine的控制結構,是對goroutine的抽象,其中goid是這個goroutine的id,status是這個goroutine的狀态。

     上面是G結構體的部分域,可以看到一些主要資訊,棧資訊,這是控制goroutine需要多大的棧,利于連續棧技術的棧控件配置設定

      lockedm是鎖定m隻能在綁定的上運作,後面我們會了解到m到底是什麼。

     goroutine在切換的時候,上下文儲存在結構體sched域中,goroutine是輕量級的,切換時并不會陷入到作業系統核心中,是以儲存過程很輕量。

struct Gobuf
{
    // The offsets of these fields are known to (hard-coded in) libmach.
    uintptr    sp;
    byte*    pc;
    G*    g;
    ...
};
           

看上面的gobuf結構體,隻儲存了目前棧指針和程式計數寄存器以及g本身。

g有一個全局的g清單

結構體M

M是machine的縮寫,是對機器的抽象,一個m對應一條作業系統的實體線程。M必須關聯p才能執行go代碼,但是當他處理阻塞或者系統調用中時。可以不用關聯p。

struct M
{
    G*    g0;        // 帶有排程棧的goroutine
    G*    gsignal;    // signal-handling G 處理信号的goroutine
    void    (*mstartfn)(void);
    G*    curg;        // M中目前運作的goroutine
    P*    p;        // 關聯P以執行Go代碼 (如果沒有執行Go代碼則P為nil)
    P*    nextp;
    int32    id;
    int32    mallocing; //狀态
    int32    throwing;
    int32    gcing;
    int32    locks;
    int32    helpgc;        //不為0表示此m在做幫忙gc。helpgc等于n隻是一個編号
    bool    blockingsyscall;
    bool    spinning;
    Note    park;
    M*    alllink;    // 這個域用于連結allm
    M*    schedlink;
    MCache    *mcache;
    G*    lockedg;
    M*    nextwaitm;    // next M waiting for lock
    GCStats    gcstats;
    ...
};
           

 上面是m的部分域,m也有一個全局的m表,控制這所有的m,lockedg是某情況下,g鎖定在這麼m中運作不會切換到其他的m,其實就是g綁定特定的m運作。m中有一個mcache,是目前m的記憶體緩存。m和g一樣有一個常用寄存器,代表目前的m,同時存在多個,辨別同時存在多個實體線程。

結構體m可以看到有兩個g,一個是curg,代表目前m綁定的結構體g,另一個g0,是待遇排程棧的goroutine,這是一個比較特殊的goroutine,普通的goroutine的棧是在堆上的可增長的棧,而g0是在m對應的線程棧上,所有的排程相關的代碼會切換到該goroutine棧中再執行。

結構體p

struct P
{
    Lock;
    uint32    status;  // Pidle或Prunning等
    P*    link;
    uint32    schedtick;   // 每次排程時将它加一
    M*    m;    // 連結到它關聯的M (nil if idle)
    MCache*    mcache;
    G*    runq[256];
    int32    runqhead;
    int32    runqtail;
    // Available G's (status == Gdead)
    G*    gfree;
    int32    gfreecnt;
    byte    pad[64];
};
           

注意,跟G不同的是,P不存在

waiting

狀态。MCache被移到了P中,但是在結構體M中也還保留着。在P中有一個Grunnable的goroutine隊列,這是一個P的局部隊列。當P執行Go代碼時,它會優先從自己的這個局部隊列中取,這時可以不用加鎖,提高了并發度。如果發現這個隊列空了,則去其它P的隊列中拿一半過來,這樣實作工作流竊取的排程。這種情況下是需要給調用器加鎖的。

Sched

struct Sched {
    Lock;
    uint64    goidgen;
    M*    midle;     // idle m's waiting for work
    int32    nmidle;     // number of idle m's waiting for work
    int32    nmidlelocked; // number of locked m's waiting for work
    int3    mcount;     // number of m's that have been created
    int32    maxmcount;    // maximum number of m's allowed (or die)
    P*    pidle;  // idle P's
    uint32    npidle;  //idle P的數量
    uint32    nmspinning;
    // Global runnable queue.
    G*    runqhead;
    G*    runqtail;
    int32    runqsize;
    // Global cache of dead G's.
    Lock    gflock;
    G*    gfree;
    int32    stopwait;
    Note    stopnote;
    uint32    sysmonwait;
    Note    sysmonnote;
    uint64    lastpoll;
    int32    profilehz;    // cpu profiling rate
}
           

大多數需要的資訊都已放在了結構體M、G和P中,Sched結構體隻是一個殼。可以看到,其中有M的idle隊列,P的idle隊列,以及一個全局的就緒的G隊列。Sched結構體中的Lock是非常必須的,如果M或P等做一些非局部的操作,它們一般需要先鎖住排程器。

go