【每日筆記】【Go學習筆記】2018-12-28 go語法筆記與MPG

李樂

1.數組與切片

1.1數組

和以往認知的數組有很大不同。

數組是值類型,賦值和傳參會複製整個數組;
數組長度必須是常量,且是類型的組成部分。[2]int 和 [3]int 是不同類型;
指針數組 [n]T(數組每個元素都是指針),數組指針 [n]T(指向數組的指針);
內置函數 len 和 cap 都返回數組⻓長度 (元素數量)。

a := [3]int{1, 2} // 未初始化元素值爲 0。
b := [...]int{1, 2, 3, 4} // 通過初始化值確定數組⻓長度。
d := [...]struct {
    name string
    age uint8
}{
    {"user1", 10},
    {"user2", 20}, // 別忘了最後一⾏的逗號。
}
 
println(len(a), cap(a)) //3,3

1.2切片(slice)

slice並不是數組或數組指針,它通過內部指針和相關屬性引用數組⽚片段,以實現變長方案;slice是引用類型,但⾃自⾝身是結構體,值拷貝傳遞。定義如下:

struct  Slice
{               // must not move anything
    byte*   array;      // actual data
    uintgo  len;        // number of elements
    uintgo  cap;        // allocated number of elements
};

代碼舉例:

data := [...]int{0, 1, 2, 3, 4, 5, 6}
slice := data[1:4:5] // [low : high : max]

clipboard.png

注意:這裏的max不能超過數組最大索引,否則編譯會報錯;讀寫操作實際目標是底層數組;

也可直接創建 slice 對象,會自動分配底層數組

s1 := []int{0, 1, 2, 3, 8: 100} // 通過初始化表達式構造,可使⽤用索引號。
s2 := make([]int, 6, 8) // 使用 make 創建,指定 len 和 cap 值。
s3 := make([]int, 6) // 省略 cap,相當於 cap = len。

向 slice 尾部添加數據,返回新的 slice 對象,此時兩個slice底層公用同一個數組;但是一旦超出原 slice.cap 限制,就會重新分配底層數組。

s := make([]int, 0, 5)
fmt.Printf("%p\n", &s)
 
s2 := append(s, 1)
fmt.Printf("%p\n", &s2)
 
fmt.Println(s, s2)
 
/*
0x210230000
0x210230040
[] [1]
*/

2.結構體、方法

包內函數名稱首字母大寫時,可以被其他包訪問,否則不能;

函數調用:包名.函數名稱

方法調用:結構體變量或指針.函數名稱。

結構體:

type Node struct {
    id int
    data *byte
    next *Node
}

函數:

func test(x, y int, s string) (int, string) { // 類型相同的相鄰參數可合併,(int,string)多返回值必須⽤用括號
    n := x + y
    return n, fmt.Sprintf(s, n)
}
 
//變參
func test(s string, n ...int) string {
    var x int
    for _, i := range n {
        x += i
    }
 
    return fmt.Sprintf(s, x)
}
 
 
func main() {
    s := []int{1, 2, 3}
    println(test("sum: %d", s...))   //使用 slice 對象做變參時,必須展開
}
//不能⽤用容器對象接收多返回值。只能⽤用多個變量,或 "_" 忽略
func test() (int, int) {
    return 1, 2
}
 
func main() {
    // s := make([]int, 2)
    // s = test() // Error: multiple-value test() in single-value context
    x, _ := test()
    println(x)
}
//命名返回參數可看做與形參類似的局部變量,最後由 return 隱式返回
func add(x, y int) (z int) {
    z = x + y
    return
}

方法(隸屬於結構體?):

type Data struct{
    x int
}
 
func (self Data) ValueTest() { // func ValueTest(self Data);
    fmt.Printf("Value: %p\n", &self)
}
 
func (self *Data) PointerTest() { // func PointerTest(self *Data);
    fmt.Printf("Pointer: %p\n", self)
}
 
func main() {
    d := Data{}
    p := &d
    fmt.Printf("Data: %p\n", p)
 
    d.ValueTest() // ValueTest(d)
    d.PointerTest() // PointerTest(&d)
    p.ValueTest() // ValueTest(*p)
    p.PointerTest() // PointerTest(p)
}
 
/*
Data : 0x2101ef018
Value : 0x2101ef028
Pointer: 0x2101ef018
Value : 0x2101ef030
Pointer: 0x2101ef018
*/

3.codis環境安裝:

https://github.com/CodisLabs/...

4.MPG

  • G: 表示goroutine,存儲了goroutine的執行stack信息、goroutine狀態以及goroutine的任務函數等;
  • P: 表示邏輯processor,P的數量決定了系統內最大可並行的G的數量(前提:系統的物理cpu核數>=P的數量);P的最大作用還是其擁有的各種G對象隊列、鏈表、一些cache和狀態。
  • M: M代表着真正的執行計算資源。在綁定有效的p後,進入schedule循環;而schedule循環的機制大致是從各種隊列、p的本地隊列中獲取G,切換到G的執行棧上並執行G的函數,調用goexit做清理工作並回到m,如此反覆。M並不保留G狀態,這是G可以跨M調度的基礎。

clipboard.png

P是一個“邏輯Proccessor”,每個G要想真正運行起來,首先需要被分配一個P(進入到P的local runq中,這裏暫忽略global runq那個環節)。對於G來說,P就是運行它的“CPU”,可以說:G的眼裏只有P。但從Go scheduler視角來看,真正的“CPU”是M,只有將P和M綁定才能讓P的runq中G得以真實運行起來。

G-P-M模型的實現算是Go scheduler的一大進步,但Scheduler仍然有一個頭疼的問題,那就是不支持搶佔式調度,導致一旦某個G中出現死循環或永久循環的代碼邏輯,那麼G將永久佔用分配給它的P和M,位於同一個P中的其他G將得不到調度,出現“餓死”的情況。更爲嚴重的是,當只有一個P時(GOMAXPROCS=1)時,整個Go程序中的其他G都將“餓死”。

Go 1.2中實現了“搶佔式”調度。這個搶佔式調度的原理則是在每個函數或方法的入口,加上一段額外的代碼,讓runtime有機會檢查是否需要執行搶佔調度。這種解決方案只能說局部解決了“餓死”問題,對於沒有函數調用,純算法循環計算的G,scheduler依然無法搶佔。

Go程序啓動時,runtime會去啓動一個名爲sysmon的m(一般稱爲監控線程),該m無需綁定p即可運行,該m在整個Go程序的運行過程中至關重要:

  • 向長時間運行的G任務發出搶佔調度;
  • 收回因syscall長時間阻塞的P;
  • ……

如果G被阻塞在某個system call操作上,那麼不光G會阻塞,執行該G的M也會解綁P(實質是被sysmon搶走了),與G一起進入sleep狀態。如果此時有idle的M,則P與其綁定繼續執行其他G;如果沒有idle M,但仍然有其他G要去執行,那麼就會創建一個新M。

4.1線程/協程實驗

package main
import (
    "fmt"
    "time"
    "runtime"
)
func main() {
    runtime.GOMAXPROCS(48)
    for i:=0;i<10000;i++ {
        go func() {
           fmt.Println("start ", i)
           time.Sleep(time.Duration(10)*time.Second)
        }()
    }
    time.Sleep(time.Duration(2000)*time.Second)
    fmt.Println("main end")
}

使用命令ps -eLf |grep test | wc -l查看程序線程數目。fmt.Println底層通過write系統調用向標準輸出寫數據。

1 5
5 41
10 493
25 760
48 827

邏輯處理器的數目即程序最大可真正並行的G數目(小於機器核數),所以隨着邏輯處理器數目增加,並行向標準輸出寫數據阻塞時間越長,導致sysmon監控線程分離阻塞的M與P,同時創建新的M即線程。

4.2搶佔調度測試

4.2.1死循環

package main
 
import (
    "fmt"
    "runtime"
    "time"
)
 
 
func deadloop() {
    for {
         
    }
}
 
func main() {
    runtime.GOMAXPROCS(1)
    go deadloop()
    for {
        time.Sleep(time.Second * 1)
        fmt.Println("I got scheduled!")
    }
}

測試結果:始終沒有輸出"I got scheduled!";因爲只有一個邏輯處理器,且一直在執行協程deadloop,main協程無法搶佔。

4.2.2死循環加函數調用

package main
 
import (
    "fmt"
    "runtime"
    "time"
)
 
 
func add(a, b int) int {
    return a + b
}
 
func dummy() {
    add(3, 5)
}
 
func deadloop() {
    for {
        dummy()
    }
}
 
func main() {
    runtime.GOMAXPROCS(1)
    go deadloop()
    for {
        time.Sleep(time.Second * 1)
        fmt.Println("I got scheduled!")
    }
}

測試結果:

root@nikel ~/gocoder$./deadloop
I got scheduled!
I got scheduled!
I got scheduled!
I got scheduled!

詳情參考文章

https://tonybai.com/2017/06/2...

https://tonybai.com/2017/11/2...

寫的挺好的,值得學習學習。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章