李乐
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]
注意:这里的 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 调度的基础。
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…
写的挺好的,值得学习学习。