复习
- Go语言保留了C语言中的指针,但又有所不同;
- 默认值为
nil
- 操作符
&
取变量地址,*
通过指针访问目标对象; - 不支持指针运算,不支持
->
运算符,直接使用.
访问目标成员。
- 默认值为
- 32为系统:
4G
-
3G - 4G
:kernel
占用 -
0 - 3G
:用户使用,可分为代码区、只读数据区、数据区、未初始化数据区、堆、栈、其他
-
栈
:默认大小为1M
,Windows系统上可扩展到8M
,考虑到栈区内存很小,所以由操作系统管理栈的释放; -
堆
:通常内存空间在1G
以上,在程序合理的情况下,GC机制能有效控制释放;
-
- 切片并不是数组或数组指针,它是通过内部指针和相关属性引用数组片段,以实现变长放案;
- 切片并不是真正意义上的动态数组,而是一个引用类型,数组是值传递;
- 切片是一种数据结构体,总是指向一个底层
array
- 切片创建的多种方式
slice := []int{1, 2, 3} slice := make([]int, 长度, 容量) slice := make([]int, 长度) // 省略容量时,容量==长度
-
make()
函数只能用于创建切片、Map、Channel
- 相同结构体类型:成员变量的类型、个数、顺序都完全一致;相同结构体变量之间可以直接赋值;
- 结构体变量的地址 == 结构体首个元素的地址
- 结构体是值传递,内存消耗大,效率低下;
-
unsave.Sizeof(xxx)
:查看占用的字节数;- Go语言的布尔类型是用
0/1
模拟的,所以占用1
个字节; - 不管何种类型的指针,在64位操作系统下,占用字节数恒为
8
,所以在传递结构体时,通常使用指针
实现地址传递。
- Go语言的布尔类型是用
- 函数不能返回局部变量的地址值(指针)!因为局部变量保存在栈中,栈区释放之后,局部变量不受系统保护,随时可能把内存分配给其他程序;
- 虚拟地址映射:我们常说的内存地址其实虚拟地址,因为操作系统并不是希望直接把物理内存地址暴露给用户;
- 堆区的地址是连续的,而真正的物理存储却并不是连续的内存区间,目的是为了让内存得到充分的利用;
- 磁盘的最小单位是
扇区
,内存的最小单位是页
。
- 当一个进程启动时,系统会自动打开三个文件:标准输入、标准输出、标准错误
--- stdin、stdout、stderr
,进程结束,系统会自动关闭它们。
并发
- 并行与并发
- 并行:借助多核CPU实现,是真并行;
- 并发:宏观的用户体验上,程序是并行的;微观上,其实是轮换使用CPU时间轮片,飞快地切换,是假并行。
- 程序与进程
- 程序:编译成功得到的二进制文件,占用磁盘空间;
- 进程:运行起来地程序,占用系统资源;
- 进程状态
- 5种基本状态:初始态、就绪态、运行态、挂起/阻塞态、终止/停止态
- 初始态为进程准备阶段,常与就绪态结合考虑;
- 进程并发
- 系统开销比较大,占用资源比较多,开启进程数量比较少;
- 父进程通过
fork
创建子进程,子进程再创建新的进程,且父进程永远无法预测子进程什么时候结束; - 在 unix/linux 系统下,还会产生孤儿进程和僵尸进程;
- 孤儿进程:父进程先于子进程结束,则子进程成为孤儿进程,子进程地父进程成为
init
进程,称为init
进程领养孤儿进程; - 僵尸进程:进程终止,父进程尚未回收,子进程残留资源存放于内核种,变成僵尸进程;
- Windows系统地进程和Linux地进程有所不同,它从不执行任何东西,只是为线程提供执行环境,由线程负责执行包含在进程地址空间中的代码;在创建一个进程时,系统也会自动为其创建第一个线程,称为主线程。
- 线程并发
- 进程是最小的系统资源分配单位
- 线程:LWP 轻量级的进程,最小的执行单位,CPU分配时间轮片的对象
- 线程同步
- 线程同步机制:锁
- 互斥锁:建议锁,拿到锁以后才能访问数据,没有拿到锁的线程阻塞等待;
- 读写锁:一把具有读属性和写属性的锁,写独占,读共享,且写的优先级最高。
- 协程并发:轻量级线程,占用系统资源最少
- 协程最大的优势在于轻量级,可以轻松创建数万个而不导致系统资源衰竭,而线程和进程通常很难达到1万个;
- 一个线程可以有任意多个协程,但某一个时刻只能有一个协程在运行,多个协程共享该线程分配到的计算机资源;
- 在协程中,调用一个任务就像调用一个函数一样,消耗的系统资源最少,但能达到进程/线程并发的相同效果。
Goroutine
- Go语言为并发编程而内置的上层API基于顺序通信进程模型
CSP
,这就意味着显式锁是可以避免的,因为Go通过相对安全的通道发送和接收数据以实现同步,大大简化了并发程序的编写; - Go语言中的并发程序主要通过两种手段实现:
Goroutine
和Channel
-
Goroutine
是Go语言并行设计的核心,也成为Go程,其本质就是协程,十几个Goroutine
在底层可能只有五六个线程,Go语言内部实现了Goroutine
之间的内存共享; - 执行
Goroutine
只需极少的栈内存(4-5KB
,会根据相应的数据伸缩),可同时运行成千上万个并发任务; - 创建:只需要在函数调用语句前添加
go
关键字,就可以创建并发执行单元,调度器会自动将其安排到合适的系统线程上执行;
func sing() {
for i:=0; i<10; i++ {
fmt.Println("sing: ", i)
time.Sleep(10)
}
}
func dance() {
for i:=0; i<10; i++ {
fmt.Println("dance: ", i)
time.Sleep(10)
}
}
func main() {
go sing()
go dance()
for {
;
}
}
- 当一个程序启动时,主函数
main()
在一个单独的Goroutine
中运行,又称为main Goroutine(主Go程)
,在主Go程中开启的Go程称为子Go程; - 特性:主Go程一旦结束,进程也就结束了,子Go程也就随之退出了!
runtime包
-
runtime.Gosched()
:用于让出CPU时间片,让出当前Goroutine
的执行权限,调度器安排其他等待的任务执行,并在下次重新获得CPU时间轮片时,从之前出让CPU的位置继续向下执行!- 虽然
runtime.Gosched()
与time.Sleep()
都能让出时间片,但它们的量级是不同的; -
runtime.Gosched()
让时间片之后,会立即加入等待区,而time.Sleep()
则必须等待休眠时间到了之后才会加入等待区。
- 虽然
-
runtime.Goexit()
:立即终止当前Goroutine(非主Go程)
的执行,调度器确保所有已注册defer
延迟调用被执行;-
return
只是针对一个函数的结束,其后面注册的defer
也不会再执行,因为还没来得及注册; -
Goexit()
针对的是整个当前的Go程,一旦执行,当前所在的Go程立即终止,其后的代码不在执行,包括尚未注册的defer
-
-
GOMAXPROCS(n)
:设置可同时执行(并行)的CPU核数的最大值,并返回之前的值;n := runtime.GOMAXPROCS(1) // 将CPU设置为单核
- 首次调用返回默认值,如果
n < 1
,则设置失败,不会更改当前值; - 默认会使用CPU的全核工作,但也会受电源等元器件的影响,为了保护系统服务正常工作,而选择降频工作,即不启用全核。
- 首次调用返回默认值,如果
-
NumCPU()
:查询本地机器的逻辑CPU个数; -
GC()
:手动执行一次垃圾回收;
Channel
-
Channel
是Go语言中的一个核心类型,可以看成管道,分为两个端:写端(传入端)、读端(传出端)- 并发核心单元通过
Channel
就可以发送/接收数据进行通讯,在一定程度上进一步降低编程难度; -
Channel
是一个数据类型,主要用于解决协程的同步问题以及协程之间的数据共享(数据传递)问题; -
Goroutine
运行在相同的地址空间,因此访问共享内存必须做好同步,Goroutine
奉行通过通信来共享内存,而不是共享内存来通信; - 引用类型
Channel
可用于多个Goroutine
的通讯,其内部实现了同步,确保并发安全。
- 并发核心单元通过
-
Channel
与Map
类似,由make()
创建底层数据结构,且它是引用类型,默认值为nil
,遵循地址传递;- 声明
var channel chan int // 声明一个Channel通道,默认可读可写 var read <-chan int // 声明一个只读的Channel通道 var write chan<- int // 声明一个只写的Channel通道
- 创建
channel := make(chan Type, capacity) //创建一个Channel
-
chan
是声明Channel
的关键字,Type
表示Channel
收发数据的类型; -
capacity
可省略,默认值为0
,此时Channel
是无缓冲阻塞读写的;当capacity > 0
时,Channel
是有缓冲非阻塞的,直到写满capacity
个元素才阻塞写入。
-
Channel
通过操作符<-
来收发数据channel <- value // 写端,发送 value 到 channel,value 的数据类型与定义类型保持一致 <-channel // 接收并将其丢弃,读端 x := <-channel // 从 channel 中接收数据,并赋值给x,读端 x, ok := <-channel // ok 可以检查通道是否已关闭,或者是否为空,读端
-
len()
获取的是Channel
中剩余未读取的数据个数; -
cap()
获取Channel
的容量。
-
-
For example
var channel = make(chan int) // 创建 Channel 通道 func sing() { printer("hello") channel <- 8 // 执行完打印操作之后,再向Channel中发送数据 } func dance() { <- channel // 阻塞、等待接收Channel中的数据 printer("world") } func printer(s string) { // 打印操作,每次睡眠 1s for _,ch:=range s { fmt.Printf("%c", ch) time.Sleep(1000*time.Millisecond) } } func main() { go sing() go dance() for { ; } } // helloworld
- 无缓冲阻塞
Channel
:同步通信- 无缓冲通道是指在接收前没有能力保存任何值的通道,通道容量为
0
- 它要求写端和读端同时准备好,才能完成收发操作,否则先执行的一端Go程就会阻塞等待,即读写同步!比如打电话
func main() { ch := make(chan string) go func() { for i:=0; i<5; i++ { fmt.Println(i) time.Sleep(1000*time.Millisecond) } <- ch // 循环结束才会消费通道中的数据 }() ch <- "hello" // 写端-主Go程陷入阻塞,等待读端消费数据,然后才能重新加入CPU时间轮片 fmt.Println("main end") } // 0 1 2 3 4 main end
- 这种对通道进行发送和接收的交互行为本身就是同步的,其中任意一个操作都无法离开另一个操作单独存在,否则就会造成死锁;
- 阻塞:由于某种原因导致数据没有到达,当前协程/线程持续处于等待状态,直到条件满足,才能解除阻塞;
- 同步:在两个或多个协程/线程间,保持数据内容的一致性。
- 无缓冲通道是指在接收前没有能力保存任何值的通道,通道容量为
- 有缓冲
Channel
:异步通信- 缓冲区可以进行数据存储,达到容量上限之后才会阻塞,具备异步能力!比如发短信
func main() { ch := make(chan int, 3) go func() { for i:=0; i<8; i++ { fmt.Println("func: ", i) ch <- i } }() for i := 0; i < 8; i++ { n := <-ch fmt.Println("main: ", n) time.Sleep(1*time.Second) // 睡眠 1s } fmt.Println("main end") }
-
close(ch)
:内置函数,关闭Channel
- 一端关闭了通道,另一端是可以判断通道是否已经关闭的;
if n, ok := <-ch; ok { }
- 如果对端已经关闭了通道,
ok
为false
,主要也是关闭发送端; - 数据未发送完,不应该关闭通道;已关闭的通道不能再发送数据,否则报异常:send on closed channel
- 写端已关闭了通道,对于无缓冲的
Channel
,读端还可以从中读取到数据,结果为数据类型的默认值;对于有缓冲的Channel
,如果缓冲区内还有数据,则先读数据,读完之后还可以继续读,结果也是数据类型的默认值;
-
for-range
可以遍历通道,获取其中的数据for n := range channel { }
单向Channel
- 默认的
Channel
是双向的 - 单向写
Channel
:var send chan <- int
- 单向读
Channel
:var recv <-chan int
- 双向
Channel
可以隐式转换为任意一种同类型的单向Channel
,反之则不行!ch := make(chan int) var send chan <- int = ch send <- 89
-
Channel
一定是成对出现的,只有单向Channel
会造成死锁; - 不能对单向写
Channel
进行读取操作。
-
- 单向
Channel
传参func send(s chan <- int) { // 单向写Channel s <- 89 close(s) } func recv(r <- chan int) { // 单向读Channel n := <- r fmt.Println("recv: ", n) } func main() { ch := make(chan int) // 双向Channel go func() { send(ch) }() recv(ch) // 89 }
生产者消费者模型
生产者 -> 缓冲区 -> 消费者
- 缓冲区的作用
- 解耦:降低生产者和消费者之间的耦合度;
- 并发:生产者和消费者的数量不对等时,保持正常通信;
- 缓存:生产者和消费者的数据处理速度不一致时,暂存数据;
-
Channel
可以作为缓冲区,实现生产者(Go程-1)与消费者(Go程-2)的通信
定时器
-
time.Timer
是一个定时器,代表未来的一个单一事件,可以设置等待时间;type Timer struct { C <-chan Time r runtimeTimer }
- 它提供一个
Channel
,在定时时间到达之前,没有数据写入通道C
就会一直阻塞;定时时间到了之后,系统会自动向C
通道中写入当前时间,阻塞即刻被解除; -
time.NewTimer(d)
:创建定时器,传入定时时长,返回值为*Timer
t := time.NewTimer(time.Second*2) //定时2s now := <- t.C //从 Timer 中读通道C 的数据,也就是当前时间
- 设置了定时器后,当前Go程会阻塞,等待定时时间到,返回
Timer
指针; - 应从通道中读出数据,否则会一直阻塞;
-
t.Stop()
:停止定时器; -
t.Reset(d)
:重置定时时长;
- 它提供一个
-
timer.Sleep()
属于单纯的等待; -
timer.After()
也可以实现定时等待,返回一个Time
类型的Channel
now <- timer.After(time.Second*2) // 从通道中取出当前时间
- 周期定时:
timer.NewTicker(d)
,系统每隔d
时间自动向通道中写入一次当前系统时间,返回*Ticker
type Ticker struct { C <-chan Time r runtimeTimer }
func main() { t := time.NewTicker(time.Second) // 设置周期为 1s go func() { for { now := <-t.C fmt.Println(now) } }() for { ; } }