golang二級指標操作連結串列

sheepbao發表於2018-08-31

golang利用二級指標操作連結串列

以下所有觀點都是個人愚見,有不同建議或補充的的歡迎emial我aboutme

導讀

之前開了個專案來看golang runtime原始碼https://github.com/sheepbao/golang_runtime_reading,發現實現全域性P連結串列的程式碼挺有趣的,遂研究記錄一下。

先看個例子

package ptplist

import (
    "log"
    "testing"
    "unsafe"
)

type puintptr uintptr

//go:nosplit
func (pp puintptr) ptr() *p { return (*p)(unsafe.Pointer(pp)) }

//go:nosplit
func (pp *puintptr) set(p *p) { *pp = puintptr(unsafe.Pointer(p)) }

type p struct {
    id   int
    link puintptr
}

var pidleList puintptr 

func pidleput(_p_ *p) {
    _p_.link = pidleList
    pidleList.set(_p_)
}

func pidleget() *p {
    _p_ := pidleList.ptr()
    if _p_ != nil {
        pidleList = _p_.link
    }
    return _p_
}

func TestPtoPList(t *testing.T) {
    for i := 0; i < 5; i++ {
        tp := p{id: i}
        pidleput(&tp)
        log.Printf("put p: %d", i)
    }
    log.Println("")
    for i := 0; i < 5; i++ {
        tp := pidleget()
        log.Printf("get p: %d", tp.id)
    }

}

結果:

=== RUN   TestPtoPList
2018/08/28 17:25:22 put p: 0
2018/08/28 17:25:22 put p: 1
2018/08/28 17:25:22 put p: 2
2018/08/28 17:25:22 put p: 3
2018/08/28 17:25:22 put p: 4
2018/08/28 17:25:22 
2018/08/28 17:25:22 get p: 4
2018/08/28 17:25:22 get p: 3
2018/08/28 17:25:22 get p: 2
2018/08/28 17:25:22 get p: 1
2018/08/28 17:25:22 get p: 0
--- PASS: TestPtoPList (0.00s)

分析

從結果上看pidleList實現了一個棧,pidleput把一個新的p push到pidleListpidleget是從pidleList pop出一個p。 具體看看這個怎麼實現的:

type puintptr uintptr

首先我們定義了puintptr型別,底層型別為uintptr。uintptr是整型,可以足夠儲存指標的值得範圍,在32平臺下為4位元組,在64位平臺下是8位元組,它和unsafe.Pointer可相互轉換。 uintptr和unsafe.Pointer的區別就是:unsafe.Pointer只是單純的通用指標型別,用於轉換不同型別指標,它不可以參與指標運算;而uintptr是可用於指標運算的。

uintptr和unsafe.Pointer互轉換

func TestUintprt1(t *testing.T) {
    var a int
    a = 2018
    b := (*int)(unsafe.Pointer(&a))
    t.Logf("a.addr=%p, b.addr=%p, *b=%d", &a, b, *b)
    buintptr := uintptr((unsafe.Pointer(&a)))
    t.Logf("buintptr=0x%x", buintptr)
}

output:

a.addr=0xc42008e1e0, b.addr=0xc42008e1e0, *b=2018
buintptr=0xc42008e1e0

buintptr的值就等於a的地址

uintptr的指標運算取陣列

func TestUintprt2(t *testing.T) {
    var a [2]int
    a[0] = 2018
    a[1] = 2019

    t.Logf("a0.addr=%p, a1.addr=%p", &a[0], &a[1])
    auintptr := uintptr((unsafe.Pointer(&a)))
    t.Logf("auintptr=0x%x", auintptr)
    // 把auintptr的值加上一個指標的長度, 和C語言的p++一樣
    a1uintptr := auintptr + unsafe.Sizeof(&a)
    a1pointer := unsafe.Pointer(a1uintptr)
    // 得到a[1]
    a1 := *(*int)(a1pointer)
    t.Logf("a1pointer=%p, a1=%d", a1pointer, a1)
}

output:

a0.addr=0xc42009c1e0, a1.addr=0xc42009c1e8
auintptr=0xc42009c1e0
a1pointer=0xc42009c1e8, a1=2019

獲取到auintptr表示a陣列的首地址,把它加上一個指標長度,就會得到a陣列下一個元素。這個和C語言的p++是一樣的,只不過golang不推崇指標運算,寫起來很麻煩。

//go:nosplit
func (pp puintptr) ptr() *p { return (*p)(unsafe.Pointer(pp)) }

//go:nosplit
func (pp *puintptr) set(p *p) { *pp = puintptr(unsafe.Pointer(p)) }

接著給puintptr型別增加了兩個方法,比如現有有個puintptr型別的值pl,那麼:
ptr()方法,將pl變為p型別的指標返回,set(p *p)方法將p的地址轉換為puintptr,並將這puintptr賦值給pl,也就是說pl的值等於p的地址。 這裡的pp是pl的指標,*pl就是二級指標了。理解這兩個方法就好理解程式碼了。

func pidleput(_p_ *p) {
    // 將pidleList賦值給link ,最開始的時候pidleList為0,但是之後pidleList是上個p的地址
    _p_.link = pidleList
    // pidleList儲存_p_的地址
    pidleList.set(_p_)
}

接著定義put方法,_p_.link儲存上個p的地址,pidleList儲存當前_p_的地址,這裡就構成了單向連結串列,看圖最明顯

/*
        p1                 p2
    +-------+           +-------+
    |   id  |<-----+    |   id  |<------ pidleList
    +-------+      |    +-------+
    | link  |      +----| link  |
    +-------+           +-------+
*/

所以每當pidleput時將當前p的link指向上個p,pidleList指向最新的p

func pidleget() *p {
    // 獲取pidleList指向的p
    _p_ := pidleList.ptr()
    // _p_ 不等於0
    if _p_ != nil {
        // 將pidleList重新指向下一個p
        pidleList = _p_.link
    }
    return _p_
}

理解了上面的pidleget就簡單了。

golang 原始碼

想具體看原始碼的話可以看下面的連線:

https://github.com/sheepbao/golang_runtime_reading/blob/master/src/runtime/proc.go

https://github.com/sheepbao/golang_runtime_reading/blob/master/src/runtime/runtime2.go

相關文章