defer语句
如果一条语句前面加了一个defer, 那么这个defer语句会在当前作用域内的所有语句执行完之后再执行, 如果有多条defer语句, 那么在后面的defer语句先执行(类似于栈).
例如:
package main
import "fmt"
func main() {
a, b := 1, 2
defer fmt.Println("heihei")
defer fmt.Println("haha")
fmt.Println(a + b)
}
那么输出的顺序是:
3
haha
heihei
涉及变量的defer语句
考虑下面的语句:
package main
import "fmt"
func main() {
a, b := 1, 2
c := a + b
defer fmt.Println("first", c) //3
c = 4
defer fmt.Println("second", c) //4
c = 5
defer func() {
fmt.Println("third", c) //注意如果打印的话, 这里c是6而不是5
}()
c = 6
fmt.Println(a + b)
}
打印的结果是:
6
third 6
second 4
first 3
可以发现:
- 如果defer后面跟的是语句, 那么语句中涉及的变量会被保存一个副本, 和之后该变量的值的变化无关.
- 如果defer后面跟的是匿名函数的调用, 那么这个匿名函数函数体内的变量不会被保存副本, 只有在defer执行时才会进行计算.
时间相关的函数
时间标准库的导入是:
import "time"
获取当前时间
t := time.Now()
其中, t
是time.Time
数据类型
- 时间戳:
t.Unix()
以毫秒为单位统计运行时间
t0 := time.Now()
/* 要统计的代码 */
t1 := time.Now()
fmt.Println(t1.Sub(t0).Milliseconds())
休眠函数
以毫秒为单位进行休眠:
// 休眠50毫秒
time.Sleep(50 * time.Millisecond)
MPG并发模型
假设一个机器中有$n$个CPU, 那么这个机器就有$n$个进程.
- M: 全称是Machine, 是在用户态对于CPU的一种抽象, 一个进程对应一个Machine, 用于真正执行Goroutine.
- P: 全称是Processor, 可以理解为存储Goroutine的一个队列.
- G: 全称是Goroutine, 也叫协程, 是用户代码的运行时的状态.
协程在放到Processor之前, 会被放到一个协程的全局队列runqueue中.
- 当创建第一个协程时, 会发生:
- 系统创建一个Machine, 并绑定到一个进程上.
- 系统创建一个Processor, 并绑定到当前的Machine上.
- 协程Goroutine会被放到Processor上, 然后开始在Machine执行.
- 如果协程数量很多时, 会发生:
- 系统会创建更多的Machine, 绑定到对应的进程上.
- 系统会创建更多的Processor, 用来存储将要运行的Goroutine.
- 如果Machine在执行一个Goroutine时, 发生了I/O这种高耗时任务或系统调用, 会发生:
- 绑定到这个Machine的Processor会被切换到其他的Machine.
- 这个Machine仍在处理发生I/O的Goroutine, 直到结束.
- 结束后, 这个Machine会处于短暂的空闲状态
协程
Go语言中, 创建协程的办法是在函数调用之前加上一个go
, 例如:
package main
import "fmt"
func foo() {
fmt.Println("Hello")
}
func main() {
// 创建了一个运行foo函数的协程
go foo()
}
注意, 使用`go`创建的所有协程, 它们并不存在父子关系, 在调度层面是并列关系.
在Go语言中, 有一个主协程(也叫main
协程), 主协程和其他协程的关系是:
- 主协程结束会导致所有其他协程强制结束.
- 其他协程如果出现panic, 会导致主协程也强制结束, 这个需要避免.
主协程等待子协程
如果要让主协程等待所有子协程完成之后再结束, 可以使用sync.WaitGroup{}
.
具体例子如下:
package main
import "sync"
func main() {
wg := sync.WaitGroup{}
/* 这里的2是main协程需要等待的子协程的个数 */
wg.Add(2)
/* 创建两个子协程, 然后子协程运行结束后执行Done */
go func() {
defer wg.Done()
fmt.Println("...")
}()
go func() {
defer wg.Done()
fmt.Println("....")
}()
/* main协程等待子协程 */
wg.Wait()
}
这个WaitGroup
可以写很多个, 最终的效果等于所有写的WaitGroup拼接起来.
子协程故障不导致主协程挂
如果要子协程发生panic
后不让主协程挂掉, 只需要在可能会发生panic
的子协程的函数最前面加上这句话:
func f1() {
/* 如果发生了panic, 那么最终还会执行这个defer */
defer func() {
err := recover()
/* 如果err不是nil, 证明发生了panic, 后面是panic的处理逻辑 */
if err != nil {
fmt.Println(err)
}
}
}
临界区和锁
临界区(Critical Section): 临界区是指一个协程中的一段代码, 这段代码中涉及修改全局变量的部分.
需要保证: 同一时间段内, 有且仅有一个协程能够进入它的临界区.
实现这个原则可以通过两种方式:
对临界资源加锁, 可以使用互斥锁mutex, 在Go语言中是
sync.Mutex
.```go
import “sync”/ mutex锁是全局变量 /
var lock = sync.Mutex{}func f1() {
lock.lock()
/* 临界区代码 */
lock.unlock()
}
* 将临界区代码用等价的原子操作来实现, 原子操作保证一个协程执行临界区代码时不会被切换, 可以使用Go语言中的`sync/atomic`包.
## Channel
Channel的本质是一个固定长度的队列, 它的类型关键词是`chan type`, 其中`type`可以是`int`或者`struct{}`这种数据类型.
* Channel可以使用`make`创建:
```go
// 创建一个管道, 里面元素是bool类型, 大小是3
ch := make(chan bool, 3)
写Channel, 可以使用:
ch<-你要写的元素
- 注意: 当你向Channel中写入元素后, 如果这个Channel原始为空, 那么它会唤醒所有等待这个Channel读取的协程, 相当于内置实现了生产者-消费者算法.
读Channel, 可以使用
<-ch
.注意1:
<-ch
这个整体是一个表达式, 这个整体就代表从Channel中读取的队头元素.注意2: 此时需要处理异常:
a, ok := <-ch // 当且仅当ch被close, 并且ch为空时, ok才是false, 此时需要处理异常
注意3: 当使用
<-ch
后, 如果ch
为空, 那么该协程会被阻塞, 直到有协程写入.
- 关闭Channel可以使用:
close(ch)
, 注意: 关闭Channel仅代表不可以向Channel中写入元素, 但是还可以读取.
使用Channel实现主协程等待子协程
借助Channel可以阻塞协程的特性, 可以实现主协程等待一个子协程:
func main() {
/* 1. 创建一个没有buffer的Channel */
ch := make(chan struct{}, 0)
/* 一个子协程 */
go func() {
/* 协程代码 */
/* 3. 需要被等待的子协程再结束后写入Channel, 唤醒main协程 */
defer ch<-struct{}{}
}
/* 2. 读取Channel, 由于Channel容量为0, main协程会被阻塞 */
<-ch
}
为什么Channel的数据类型是
struct{}
?
因为struct{}
这种类型在创建实例struct{}{}
时, 并不实际占用内存.
什么是没有Buffer的Channel
如果使用ch := make(chan struct{}, 0)
或者ch := make(chan struct{})
创建一个Channel, 那么这个Channel就是没有Buffer的, 此时, 向这个Channel中读/写元素都会导致Channel被阻塞.
死锁
例子1
package main
import (
"fmt"
)
func main() {
ch := make(chan struct{}, 0)
go func() {
/* 向没有Buffer的Channel写入会阻塞协程 */
ch <- struct{}{}
fmt.Println("Hello")
}()
/* main协程也会被阻塞 */
ch <- struct{}{}
}
在这个例子中, 子协程和main协程都会被永远阻塞, go语言会爆出deadlock的运行时异常.
例子2
package main
import (
"fmt"
"time"
)
func main() {
ch := make(chan struct{}, 0)
go func() {
/* 向没有Buffer的Channel写入会阻塞协程 */
ch <- struct{}{}
fmt.Println("Hello")
}()
/* 增加一个子协程 */
go func() {
time.Sleep(1 * time.Hour)
}()
/* main协程也会被阻塞 */
ch <- struct{}{}
}
这个例子在上面的基础上增加了一个休眠1小时的子协程, 运行这个代码, go语言居然就不会爆deadlock的Error了.
这是因为: 增加一个子协程后, 在运行时子协程休眠了, 但是Go语言不知道它后面会执行什么代码, 有可能要执行的代码会帮助解除main协程或前面协程的阻塞, 因此是有希望不出现deadlock的, 所以go语言没有爆deadlock.
因此:
如果Go代码没有爆deadlock, 不代表它真的没有deadlock, Go语言会假设现在还没有结束的协程可能会帮助解决deadlock, 因此要注意代码逻辑.