4

李乐

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...

写的挺好的,值得学习学习。


AI及LNMPRG研究
7.2k 声望12.8k 粉丝

一群热爱代码的人 研究Nginx PHP Redis Memcache Beanstalk 等源码 以及一群热爱前端的人