主頁 > 後端開發 > 從原始碼分析 Go 語言使用 cgo 導致的執行緒增長

從原始碼分析 Go 語言使用 cgo 導致的執行緒增長

2023-06-06 07:44:29 後端開發

TDengine Go 連接器 https://github.com/taosdata/driver-go 使用 cgo 呼叫 taos.so 中的 API,使用程序中發現執行緒數不斷增長,本文從一個 cgo 呼叫開始決議 Go 原始碼,分析造成執行緒增長的原因,

轉換 cgo 代碼

對 driver-go/wrapper/taosc.go 進行轉換

go tool cgo taosc.go

執行后生成 _obj 檔案夾

go 代碼分析

taosc.cgo1.goTaosResetCurrentDB 為例來分析,

// TaosResetCurrentDB void taos_reset_current_db(TAOS *taos);
func TaosResetCurrentDB(taosConnect unsafe.Pointer) {
    func() { _cgo0 := /*line :161:26*/taosConnect; _cgoCheckPointer(_cgo0, nil); _Cfunc_taos_reset_current_db(_cgo0); }()
}

//go:linkname _cgoCheckPointer runtime.cgoCheckPointer
func _cgoCheckPointer(interface{}, interface{})

//go:cgo_unsafe_args
func _Cfunc_taos_reset_current_db(p0 unsafe.Pointer) (r1 _Ctype_void) {
    _cgo_runtime_cgocall(_cgo_453a0cad50ef_Cfunc_taos_reset_current_db, uintptr(unsafe.Pointer(&p0)))
    if _Cgo_always_false {
        _Cgo_use(p0)
    }
    return
}

//go:linkname _cgo_runtime_cgocall runtime.cgocall
func _cgo_runtime_cgocall(unsafe.Pointer, uintptr) int32

//go:cgo_import_static _cgo_453a0cad50ef_Cfunc_taos_reset_current_db
//go:linkname __cgofn__cgo_453a0cad50ef_Cfunc_taos_reset_current_db _cgo_453a0cad50ef_Cfunc_taos_reset_current_db
var __cgofn__cgo_453a0cad50ef_Cfunc_taos_reset_current_db byte
var _cgo_453a0cad50ef_Cfunc_taos_reset_current_db = unsafe.Pointer(&__cgofn__cgo_453a0cad50ef_Cfunc_taos_reset_current_db)

  • TaosResetCurrentDB 首先呼叫 _cgoCheckPointer 檢查傳入引數是否為 nil
  • //go:linkname _cgoCheckPointer runtime.cgoCheckPointer 表示 cgoCheckPointer 方法實作是 runtime.cgoCheckPointer,如果傳入引數是 nil 程式將會 panic
  • 接著呼叫 _Cfunc_taos_reset_current_db
  • Cfunc_taos_reset_current_db 方法中 _Cgo_always_false 在運行時會是 false,所以只分析第一句 _cgo_runtime_cgocall(_cgo_453a0cad50ef_Cfunc_taos_reset_current_db, uintptr(unsafe.Pointer(&p0)))
    • _cgo_runtime_cgocall 實作是 runtime.cgocall 這個會重點分析,
    • _cgo_453a0cad50ef_Cfunc_taos_reset_current_db 由上方最后代碼塊可以看出是 taos_reset_current_db 方法指標,
    • uintptr(unsafe.Pointer(&p0)) 表示 p0 的指標地址,
    • 由上面可以看出這句意思是呼叫 runtime.cgocall,引數為方法指標和引數的指標地址,

分析 runtime.cgocall

基于 golang 1.20.4 分析該方法

func cgocall(fn, arg unsafe.Pointer) int32 {
    if !iscgo && GOOS != "solaris" && GOOS != "illumos" && GOOS != "windows" {
        throw("cgocall unavailable")
    }

    if fn == nil {
        throw("cgocall nil")
    }

    if raceenabled {
        racereleasemerge(unsafe.Pointer(&racecgosync))
    }

    mp := getg().m // 獲取當前 goroutine 的 M
    mp.ncgocall++  // 總 cgo 計數 +1
    mp.ncgo++      // 當前 cgo 計數 +1

    mp.cgoCallers[0] = 0 // 重置追蹤

    entersyscall() // 進入系統呼叫,保存背景關系, 標記當前 goroutine 獨占 m, 跳過垃圾回收

    osPreemptExtEnter(mp) // 標記異步搶占, 使異步搶占邏輯失效

    mp.incgo = true // 修改狀態
    errno := asmcgocall(fn, arg) // 真正進行方法呼叫的地方

    mp.incgo = false // 修改狀態
    mp.ncgo-- // 當前 cgo 呼叫-1

    osPreemptExtExit(mp) // 恢復異步搶占

    exitsyscall() // 退出系統呼叫,恢復調度器控制


    if raceenabled {
        raceacquire(unsafe.Pointer(&racecgosync))
    }

    // 避免 GC 過早回收
    KeepAlive(fn)
    KeepAlive(arg)
    KeepAlive(mp)

    return errno
}

其中兩個主要的方法 entersyscallasmcgocall,接下來對這兩個方法進行著重分析,

分析 entersyscall

func entersyscall() {
    reentersyscall(getcallerpc(), getcallersp())
}

entersyscall 直接呼叫的 reentersyscall,關注下 reentersyscall 注釋中的一段:

// If the syscall does not block, that is it, we do not emit any other events.
// If the syscall blocks (that is, P is retaken), retaker emits traceGoSysBlock;

如果 syscall 呼叫沒有阻塞則不會觸發任何事件,如果被阻塞 retaker 會觸發 traceGoSysBlock,那需要了解一下多長時間被認為是阻塞,先跟到 retaker 方法,

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
        sysretake := false
        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
            }
        }
        // 從系統呼叫中搶占P
        if s == _Psyscall {
            // 如果已經超過了一個系統監控的 tick(20us),則從系統呼叫中搶占 P
            t := int64(pp.syscalltick)
            if !sysretake && int64(pd.syscalltick) != t {
                pd.syscalltick = uint32(t)
                pd.syscallwhen = now
                continue
            }
            if runqempty(pp) && sched.nmspinning.Load()+sched.npidle.Load() > 0 && pd.syscallwhen+10*1000*1000 > now {
                continue
            }
            unlock(&allpLock)
            incidlelocked(-1)
            if atomic.Cas(&pp.status, s, _Pidle) {
                if trace.enabled {
                    traceGoSysBlock(pp)
                    traceProcStop(pp)
                }
                n++
                pp.syscalltick++
                handoffp(pp)
            }
            incidlelocked(1)
            lock(&allpLock)
        }
    }
    unlock(&allpLock)
    return uint32(n)
}

從上面可以看到系統呼叫阻塞 20 多微秒會被搶占 P,cgo 被迫 handoffp,接下來分析 handoffp 方法

func handoffp(pp *p) {
    // ...
    // 沒有任務且沒有自旋和空閑的 M 則需要啟動一個新的 M
    if sched.nmspinning.Load()+sched.npidle.Load() == 0 && sched.nmspinning.CompareAndSwap(0, 1) {
        sched.needspinning.Store(0)
        startm(pp, true)
        return
    }
    // ...
}

handoffp 方法會呼叫 startm 來啟動一個新的 M,跟到 startm 方法,

func startm(pp *p, spinning bool) {
    // ...
    nmp := mget()
    if nmp == nil {
        // 沒有M可用,呼叫newm
        id := mReserveID()
        unlock(&sched.lock)

        var fn func()
        if spinning {
            fn = mspinning
        }
        newm(fn, pp, id)
        releasem(mp)
        return
    }
    // ...
}

此時如果沒有 M startm 會呼叫 newm 創建一個新的 M,接下來分析 newm 方法,

func newm(fn func(), pp *p, id int64) {
    acquirem()
    mp := allocm(pp, fn, id)
    mp.nextp.set(pp)
    mp.sigmask = initSigmask
    if gp := getg(); gp != nil && gp.m != nil && (gp.m.lockedExt != 0 || gp.m.incgo) && GOOS != "plan9" {
        lock(&newmHandoff.lock)
        if newmHandoff.haveTemplateThread == 0 {
            throw("on a locked thread with no template thread")
        }
        mp.schedlink = newmHandoff.newm
        newmHandoff.newm.set(mp)
        if newmHandoff.waiting {
            newmHandoff.waiting = false
            notewakeup(&newmHandoff.wake)
        }
        unlock(&newmHandoff.lock)
        releasem(getg().m)
        return
    }
    newm1(mp)
    releasem(getg().m)
}

func newm1(mp *m) {
    if iscgo {
        var ts cgothreadstart
        if _cgo_thread_start == nil {
            throw("_cgo_thread_start missing")
        }
        ts.g.set(mp.g0)
        ts.tls = (*uint64)(unsafe.Pointer(&mp.tls[0]))
        ts.fn = unsafe.Pointer(abi.FuncPCABI0(mstart))
        if msanenabled {
            msanwrite(unsafe.Pointer(&ts), unsafe.Sizeof(ts))
        }
        if asanenabled {
            asanwrite(unsafe.Pointer(&ts), unsafe.Sizeof(ts))
        }
        execLock.rlock()
        // 創建新執行緒
        asmcgocall(_cgo_thread_start, unsafe.Pointer(&ts))
        execLock.runlock()
        return
    }
    execLock.rlock()
    newosproc(mp)
    execLock.runlock()
}

newm 看出如果執行緒都在阻塞中則呼叫 newm1newm1 呼叫 _cgo_thread_start 創建新執行緒,

由以上分析得出當高并發呼叫 cgo 且執行時間超過 20 微秒時會創建新執行緒,

分析 asmcgocall

只分析 amd64
asm_amd64.s

TEXT ·asmcgocall(SB),NOSPLIT,$0-20
    MOVQ    fn+0(FP), AX
    MOVQ    arg+8(FP), BX

    MOVQ    SP, DX

    // 考慮是否需要切換到 m.g0 堆疊
    // 也用來呼叫創建新的 OS 執行緒,這些執行緒已經在 m.g0 堆疊中了
    get_tls(CX)
    MOVQ    g(CX), DI
    CMPQ    DI, $0
    JEQ nosave
    MOVQ    g_m(DI), R8
    MOVQ    m_gsignal(R8), SI
    CMPQ    DI, SI
    JEQ nosave
    MOVQ    m_g0(R8), SI
    CMPQ    DI, SI
    JEQ nosave
    
    // 切換到系統堆疊
    CALL    gosave_systemstack_switch<>(SB)
    MOVQ    SI, g(CX)
    MOVQ    (g_sched+gobuf_sp)(SI), SP

    // 于調度堆疊中(pthread 新創建的堆疊)
    // 確保有足夠的空間給四個 stack-based fast-call 暫存器
    // 為使得 windows amd64 呼叫服務
    SUBQ    $64, SP
    ANDQ    $~15, SP // 為 gcc ABI 對齊
    MOVQ    DI, 48(SP) // 保存 g
    MOVQ    (g_stack+stack_hi)(DI), DI
    SUBQ    DX, DI
    MOVQ    DI, 40(SP) // 保存堆疊深 (不能僅保存 SP,因為堆疊可能在回呼時被復制)
    MOVQ    BX, DI  // DI = AMD64 ABI 第一個引數
    MOVQ    BX, CX  // CX = Win64 第一個引數
    CALL    AX  // 呼叫 fn

    // 恢復暫存器、 g、堆疊指標
    get_tls(CX)
    MOVQ    48(SP), DI
    MOVQ    (g_stack+stack_hi)(DI), SI
    SUBQ    40(SP), SI
    MOVQ    DI, g(CX)
    MOVQ    SI, SP

    MOVL    AX, ret+16(FP)
    RET

nosave:
    // 在系統堆疊上運行,可能沒有 g
    // 沒有 g 的情況發生在執行緒創建中或執行緒結束中(比如 Solaris 平臺上的 needm/dropm)
    // 這段代碼和上面類似,但沒有保存和恢復 g,且沒有考慮堆疊的移動問題(因為我們在系統堆疊上,而非 goroutine 堆疊)
    // 如果已經在系統堆疊上,則上面的代碼可被直接使用,在 Solaris 上會進入下面這段代碼,
    // 使用這段代碼來為所有 "已經在系統堆疊" 的呼叫進行服務,從而保持正確性,
    SUBQ    $64, SP
    ANDQ    $~15, SP // ABI 對齊
    MOVQ    $0, 48(SP) // 上面的代碼保存了 g, 確保 debug 時可用
    MOVQ    DX, 40(SP) // 保存原始的堆疊指標
    MOVQ    BX, DI  // DI = AMD64 ABI 第一個引數
    MOVQ    BX, CX  // CX = Win64 第一個引數
    CALL    AX
    MOVQ    40(SP), SI // 恢復原來的堆疊指標
    MOVQ    SI, SP
    MOVL    AX, ret+16(FP)
    RET

這段就是將當前堆疊移到系統堆疊去執行,因為 C 需要無窮大的堆疊,在 Go 的堆疊上執行 C 函式會導致堆疊溢位,

產生問題

cgo 呼叫會將當前堆疊移到系統堆疊,并且當 cgo 高并發呼叫且阻塞超過 20 微秒時會新建執行緒,而 Go 并不會銷毀執行緒,由此造成執行緒增長,

解決方案

限制 Go 程式最大執行緒數,默認為 cpu 核數,

runtime.GOMAXPROCS(runtime.NumCPU())

使用 channel 限制 cgo 最大并發數為 cpu 核數

package thread

import "runtime"

var c chan struct{}

func Lock() {
    c <- struct{}{}
}

func Unlock() {
    <-c
}

func init() {
    c = make(chan struct{}, runtime.NumCPU())
}

針對超過 20 微秒的 cgo 呼叫進行限制:

thread.Lock()
wrapper.TaosFreeResult(result)
thread.Unlock()

轉載請註明出處,本文鏈接:https://www.uj5u.com/houduan/554317.html

標籤:Go

上一篇:驅動開發:內核實作SSDT掛鉤與摘鉤

下一篇:返回列表

標籤雲
其他(160361) Python(38201) JavaScript(25475) Java(18186) C(15237) 區塊鏈(8269) C#(7972) AI(7469) 爪哇(7425) MySQL(7234) html(6777) 基礎類(6313) sql(6102) 熊猫(6058) PHP(5873) 数组(5741) R(5409) Linux(5346) 反应(5209) 腳本語言(PerlPython)(5129) 非技術區(4971) Android(4582) 数据框(4311) css(4259) 节点.js(4032) C語言(3288) json(3245) 列表(3129) 扑(3119) C++語言(3117) 安卓(2998) 打字稿(2995) VBA(2789) Java相關(2746) 疑難問題(2699) 细绳(2522) 單片機工控(2479) iOS(2434) ASP.NET(2403) MongoDB(2323) 麻木的(2285) 正则表达式(2254) 字典(2211) 循环(2198) 迅速(2185) 擅长(2169) 镖(2155) .NET技术(1981) 功能(1967) HtmlCss(1952) Web開發(1951) C++(1929) python-3.x(1918) 弹簧靴(1913) xml(1889) PostgreSQL(1879) .NETCore(1863) 谷歌表格(1846) Unity3D(1843) for循环(1842)

熱門瀏覽
  • 【C++】Microsoft C++、C 和匯編程式檔案

    ......

    uj5u.com 2020-09-10 00:57:23 more
  • 例外宣告

    相比于斷言適用于排除邏輯上不可能存在的狀態,例外通常是用于邏輯上可能發生的錯誤。 例外宣告 Item 1:當函式不可能拋出例外或不能接受拋出例外時,使用noexcept 理由 如果不打算拋出例外的話,程式就會認為無法處理這種錯誤,并且應當盡早終止,如此可以有效地阻止例外的傳播與擴散。 示例 //不可 ......

    uj5u.com 2020-09-10 00:57:27 more
  • Codeforces 1400E Clear the Multiset(貪心 + 分治)

    鏈接:https://codeforces.com/problemset/problem/1400/E 來源:Codeforces 思路:給你一個陣列,現在你可以進行兩種操作,操作1:將一段沒有 0 的區間進行減一的操作,操作2:將 i 位置上的元素歸零。最終問:將這個陣列的全部元素歸零后操作的最少 ......

    uj5u.com 2020-09-10 00:57:30 more
  • UVA11610 【Reverse Prime】

    本人看到此題沒有翻譯,就附帶了一個自己的翻譯版本 思考 這一題,它的第一個要求是找出所有 $7$ 位反向質數及其質因數的個數。 我們應該需要質數篩篩選1~$10^{7}$的所有數,這里就不慢慢介紹了。但是,重讀題,我們突然發現反向質數都是 $7$ 位,而將它反過來后的數字卻是 $6$ 位數,這就說明 ......

    uj5u.com 2020-09-10 00:57:36 more
  • 統計區間素數數量

    1 #pragma GCC optimize(2) 2 #include <bits/stdc++.h> 3 using namespace std; 4 bool isprime[1000000010]; 5 vector<int> prime; 6 inline int getlist(int ......

    uj5u.com 2020-09-10 00:57:47 more
  • C/C++編程筆記:C++中的 const 變數詳解,教你正確認識const用法

    1、C中的const 1、區域const變數存放在堆疊區中,會分配記憶體(也就是說可以通過地址間接修改變數的值)。測驗代碼如下: 運行結果: 2、全域const變數存放在只讀資料段(不能通過地址修改,會發生寫入錯誤), 默認為外部聯編,可以給其他源檔案使用(需要用extern關鍵字修飾) 運行結果: ......

    uj5u.com 2020-09-10 00:58:04 more
  • 【C++犯錯記錄】VS2019 MFC添加資源不懂如何修改資源宏ID

    1. 首先在資源視圖中,添加資源 2. 點擊新添加的資源,復制自動生成的ID 3. 在解決方案資源管理器中找到Resource.h檔案,編輯,使用整個專案搜索和替換的方式快速替換 宏宣告 4. Ctrl+Shift+F 全域搜索,點擊查找全部,然后逐個替換 5. 為什么使用搜索替換而不使用屬性視窗直 ......

    uj5u.com 2020-09-10 00:59:11 more
  • 【C++犯錯記錄】VS2019 MFC不懂的批量添加資源

    1. 打開資源頭檔案Resource.h,在其中預先定義好宏 ID(不清楚其實ID值應該設定多少,可以先新建一個相同的資源項,再在這個資源的ID值的基礎上遞增即可) 2. 在資源視圖中選中專案資源,按F7編輯資源檔案,按 ID 型別 相對路徑的形式添加 資源。(別忘了先把檔案拷貝到專案中的res檔案 ......

    uj5u.com 2020-09-10 01:00:19 more
  • C/C++編程筆記:關于C++的參考型別,專供新手入門使用

    今天要講的是C++中我最喜歡的一個用法——參考,也叫別名。 參考就是給一個變數名取一個變數名,方便我們間接地使用這個變數。我們可以給一個變數創建N個參考,這N + 1個變數共享了同一塊記憶體區域。(參考型別的變數會占用記憶體空間,占用的記憶體空間的大小和指標型別的大小是相同的。雖然參考是一個物件的別名,但 ......

    uj5u.com 2020-09-10 01:00:22 more
  • 【C/C++編程筆記】從頭開始學習C ++:初學者完整指南

    眾所周知,C ++的學習曲線陡峭,但是花時間學習這種語言將為您的職業帶來奇跡,并使您與其他開發人員區分開。您會更輕松地學習新語言,形成真正的解決問題的技能,并在編程的基礎上打下堅實的基礎。 C ++將幫助您養成良好的編程習慣(即清晰一致的編碼風格,在撰寫代碼時注釋代碼,并限制類內部的可見性),并且由 ......

    uj5u.com 2020-09-10 01:00:41 more
最新发布
  • 從原始碼分析 Go 語言使用 cgo 導致的執行緒增長

    TDengine Go 連接器 使用 cgo 呼叫 taos.so 中的 API,使用程序中發現執行緒數不斷增長,本文從一個 cgo 呼叫開始決議 Go 原始碼,分析造成執行緒增長的原因。 ## 轉換 cgo 代碼 對 driver-go/wrapper/taosc.go 進行轉換 `go tool cg ......

    uj5u.com 2023-06-06 07:44:29 more
  • 驅動開發:內核實作SSDT掛鉤與摘鉤

    在前面的文章`《驅動開發:內核決議PE結構匯出表》`中我們封裝了兩個函式`KernelMapFile()`函式可用來讀取內核檔案,`GetAddressFromFunction()`函式可用來在匯出表中尋找指定函式的匯出地址,本章將以此為基礎實作對特定`SSDT`函式的`Hook`掛鉤操作,與`《驅... ......

    uj5u.com 2023-06-06 07:43:02 more
  • C++面試八股文:如何在堆上和堆疊上分配一塊記憶體?

    某日二師兄參加XXX科技公司的C++工程師開發崗位6面: > 面試官: 如何在堆上申請一塊記憶體? > > 二師兄:常用的方法有malloc,new等。 > > 面試官:兩者有什么區別? > > 二師兄:malloc是向作業系統申請一塊記憶體,這塊記憶體沒有經過初始化,通常需要使用memset手動初始化。 ......

    uj5u.com 2023-06-06 07:42:49 more
  • 驅動開發:內核實作SSDT掛鉤與摘鉤

    在前面的文章`《驅動開發:內核決議PE結構匯出表》`中我們封裝了兩個函式`KernelMapFile()`函式可用來讀取內核檔案,`GetAddressFromFunction()`函式可用來在匯出表中尋找指定函式的匯出地址,本章將以此為基礎實作對特定`SSDT`函式的`Hook`掛鉤操作,與`《驅... ......

    uj5u.com 2023-06-05 09:18:22 more
  • 6種限流方式

    服務限流,是指通過控制請求的速率或次數來達到保護服務的目的,在微服務中,我們通常會將它和熔斷、降級搭配在一起使用,來避免瞬時的大量請求對系統造成負荷,來達到保護服務平穩運行的目的。下面就來看一看常見的6種限流方式,以及它們的實作與使用。 ......

    uj5u.com 2023-06-05 09:07:09 more
  • 6種限流方式

    服務限流,是指通過控制請求的速率或次數來達到保護服務的目的,在微服務中,我們通常會將它和熔斷、降級搭配在一起使用,來避免瞬時的大量請求對系統造成負荷,來達到保護服務平穩運行的目的。下面就來看一看常見的6種限流方式,以及它們的實作與使用。 ......

    uj5u.com 2023-06-05 08:50:27 more
  • day08-SpringCloud Gateway-服務網關

    # SpringCloud Gateway-服務網關 ## 1.Gateway介紹 ### 1.1引出問題 **沒有使用網關服務時:** **使用網關服務后:** ### 1.2Gateway網路拓撲圖 ![Gateway網路拓撲圖](https://liyuelian.oss-cn-shenzhe ......

    uj5u.com 2023-06-05 07:52:43 more
  • 【python基礎】復雜資料型別-串列型別(元組)

    # 1.初識元組 串列非常適合用于存盤在程式運行期間可能變化的資料集。串列是可以修改的。 然而,有時候需要創建一系列不可修改的元素,元組可以滿足這種需求 python將不能修改的值稱為不可變的,而不可變的串列被稱為元組。 元組看起來猶如串列,但使用圓括號而不是方括號來標識。 其語法格式:元組變數名 ......

    uj5u.com 2023-06-05 07:52:12 more
  • 【pandas基礎】--索引和軸

    在`pandas`中,索引(`index`)是用于訪問資料的關鍵。 它為資料提供了基于標簽的訪問能力,類似于字典,可以根據標簽查找和訪問資料。 而`pandas`的軸(`axis`)是指資料表中的一個維度,可以理解為表格中的行和列。 通過指定軸,我們可以對資料進行切片、篩選、聚合等操作。 下面簡要介 ......

    uj5u.com 2023-06-05 07:52:02 more
  • p4 FileReader 和 FileWriter

    # FileReader 和 FileWriter ### 一、 FileReader 和 File Writer 介紹 FileReader 和 FileWriter 是字符流,即按照字符來操作 io ### 二、 FileReader 相關方法 ![](https://img2023.cnblo ......

    uj5u.com 2023-06-05 07:51:54 more