原站地址:Go语言核心36讲_Golang_Go语言-极客时间
一、测试的基本规则和流程
GO程序主要分三类测试:功能测试、性能测试,以及示例测试。
示例测试和功能测试差不多,但它更关注程序打印出来的内容。
测试文件的名称应该以被测源码文件的名称为前导,并以“_test”为后缀。
例如,如果被测文件的名称为 demo52.go,那么测试文件的名称就是 demo52_test.go。
对测试函数的名称和签名都有哪些规定?
功能测试: 函数名称以Test为前缀,参数列表中有一个*testing.T类型的参数。
能能测试: 函数名称以Benchmark为前缀,参数列表中有一个*testing.B类型的参数。
示例测试: 函数名称以Example为前缀,对参数列表没有强制规定。
- go test命令执行的测试流程是怎样?
(1) 检查内部命令、源码文件有效性,标记是否合法。
(2) 对每个被测代码包,依次地进行构建。
(3) 执行符合要求的测试函数。
(4) 清理临时文件,打印测试结果
功能测试下,多个代码包是并发进行的。
性能测试下,多个代码包是串行进行的。
性能测试,是在所有构建步骤都做完之后,go test命令才会真正地开始进行。多个文件,多个函数,都是串行地逐个执行。目的是保证独立执行,性能测试准确。
测试结果包含三部分:运行情况,测试文件路径,耗时
$ go test puzzlers/article20/q2
ok puzzlers/article20/q2 0.008s
代码没有变动情况下,go test命令 会执行把之前缓存的结果 打印出来,时间变成 "cached"。不会重复执行。
go clean -cache 命令可以手动删除缓存数据。
设置值gocacheverify=1 将会导致 go 命令绕过任何的缓存数据。
- 如果测试失败了:
(1) go test命令并不会进行缓存
(2) 测试日志会被打印出来 (t.Log,t.Error)
t.Fatal 和t.Fatalf ,作用是打印失败错误日志之后,立即终止测试函数并宣告测试失败
解释输入性能测试命令,比如:go test -bench=. -run=^$ puzzlers/article20/q3
-bench:标明是执行性能测试。 -bench=.:带上 =. 表示 执行所有名称的性能测试函数
-run:标明是执行功能测试。-run=^$:带上=^$ 表示执行所有名称为空的功能测试函数,也就是** 不执行**功能测试函数。 不输入这个的话,默认是会执行的,但性能测试下我们需要不执行。
性能测试的结果,包含了什么数据?
$ go test -bench=. -run=^$ puzzlers/article20/q3
goos: darwin
goarch: amd64
pkg: puzzlers/article20/q3
BenchmarkGetPrimes-8 500000 2314 ns/op
PASS
ok puzzlers/article20/q3 1.192s
主要是在其中一句:BenchmarkGetPrimes-8 500000 2314 ns/op
BenchmarkGetPrimes-8:执行了性能测试函数GetPrimes, 同时运行 goroutine 的逻辑 CPU 数量是 8
500000: 函数运行时间不超过上限(默认1秒)的条件下,能执行多少次。
2314 ns/op: 单次执行GetPrimes函数的平均耗时。
二、更多的测试手法
go test -cpu P : 设置测试使用多少个CPU
P 代表着 Go 运行时系统同时运行 goroutine 的数目,也可以视为逻辑 CPU 的最大个数.
go test -parallel x : 设置功能测试函数的最大并发执行数。默认值是上面的P。
这个命令只用于功能测试,对性能测试无效。
性能测试中,可以通过 b.StartTimer和b.StopTimer 的联合运用,去除掉部分代码的执行时间。
也可以用b.ResetTimer 去除在调用它之前那些代码的执行时间。
三、sync.Mutex与sync.RWMutex
同步的用途: 避免多个线程同时操作一个数据块 或 一个代码块。
数据块和代码块合称 共享资源。
一个代码片段需要实现对共享资源的串行化访问(独占),就可以被视为一个临界区。
这样的代码片段有多个,就被称为相关临界区。
Go 中,可选择的同步工具不少。其中最重要且最常用的,当属 互斥锁(sync.Mutex)
互斥锁要求:每当 goroutine 想进入临界区时,都需要对mutex进行锁定 mu.Lock();
goroutine 离开临界区时,都要对mutex进行解锁 mu.Unlock()。
对一个已经被锁定的互斥锁进行锁定,会立即阻塞当前的 goroutine。(互斥锁能够保护临界区的原因)
Go 语言系统只要发现所有的goroutine 都处于阻塞状态,就会触发 panic。
Go 语言系统自行抛出的 panic 属于致命错误,是无法被recover函数恢复的。也就是说,一旦产生死锁,程序必然崩溃。
使用互斥锁的注意事项:
(1) 不要重复锁定
(2) 不要忘记解锁,必要时使用defer语句
(3) 不要重复解锁,不要对尚未锁定的互斥锁进行解锁(会panic)
(4) 不要在多个函数之间传递互斥锁
读写锁(sync.RWMutex)包含了两个锁,即:读锁和写锁。
Lock方法和Unlock方法对写锁进行锁定和解锁,
RLock方法和RUnlock方法对读锁进行锁定和解锁。
读写锁(sync.RWMutex)规则:
(1) 在写锁已被锁定的情况下,再锁定写锁,会阻塞当前的 goroutine。
(2) 在写锁已被锁定的情况下,锁定读锁,也会阻塞当前的 goroutine。
(3) 在读锁已被锁定的情况下,锁定写锁,同样会阻塞当前的 goroutine。
(4) 在读锁已被锁定的情况下,试图锁定读锁,并不会阻塞当前的 goroutine。
也就是,读锁是写锁的一部分,只要和写锁有关,就变成只有一个锁。
解锁一个写锁,会唤醒试图锁定读锁的 所有goroutine。
解锁一个读锁,只会唤醒试图锁定写锁的 一个goroutine。唤醒哪个,取决于等待时间。
四、条件变量sync.Cond
条件变量是基于互斥锁的,但并不是被用来保护临界区和共享资源。而是当共享资源的状态发生变化时,用来通知被互斥锁阻塞的线程。
条件变量提供的方法有三个:等待通知(wait)、单发通知(signal)和广播通知(broadcast)
条件变量怎样与互斥锁配合使用?
(1) 条件变量 配合 互斥锁 使用,需要四个组成部分,分别是:
mailbox (int8):用于标记的变量, 用于表示 共享资源的状态是否发生了变化
lock (sync.RWMutex): 互斥锁,用于保护共享资源与其上面mailbox的互斥锁
sendCond(**sync.Cond**):发送方的条件变量,发送通知 资源状态发生变化。
recvCond (**sync.Cond**):接收方的条件变量,接收通知 资源状态发生变化。
(2) sync.Cond 变量初始化的时候,需要输入 sync.Locker 指针给 sync.NewCond 函数。
sync.Locker 是接口类型,包含 Lock()和Unlock() 两个方法。
sync.Mutex 实现了 Lock()和Unlock(), 是 sync.Locker 的实现类型。
所以把 sync.Mutex传入 sync.NewCond 函数,初始化 **sync.Cond**
(3) 发送方过程,5步:
lock.Lock() //1. 互斥量进行锁定
for mailbox == 1 { //2. 循环判定标记变量是否能修改
sendCond.Wait() //2. (我方条件变量调用Wait方法进行等待)
}
mailbox = 1 //3. 修改标记变量
lock.Unlock() //4. 互斥量进行解锁
recvCond.Signal() //5. 向对方条件变量调用Signal方法的发送信号
(4) 接收方过程,5步:
lock.RLock() //1. 互斥量进行锁定
for mailbox == 0 { //2. 循环判定标记变量,是否能修改标识
recvCond.Wait() //2. 调用我方条件变量的wait方法进行等待
}
mailbox = 0 //3. 修改标记变量
lock.RUnlock() //4. 互斥量进行解锁
sendCond.Signal() //5. 调用对方条件变量的Signal方法发送信号。
- sync.Cond.Wait() 方法干了些什么?
(1) 把当前goroutine加入到我方条件变量的通知队列中。
(2) 解锁互斥量 lock (所以调用Wait()方法之前,必须先锁定互斥量lock)
(3) 把当前goroutine进入等待状态,等待唤醒,代码阻塞。
(4) goroutine被唤醒之后,锁定互斥量lock
为什么要用for语句来包裹Wait方法?
因为被唤醒不代表 标记变量 mailbox 就必然满足要求。有以下会出现仍然不满足要求的情况:
(1) 有多个 goroutine 共同在等待共享资源。
(2) 标记变量可能有多种状态。
(3) 计算机硬件问题。
Signal 只会唤醒一个goroutine,而Broadcast会唤醒所有的 goroutine。
如果条件变量通知的时候没有 goroutine 为此等待,那么该通知就会被丢弃。
wait方法需要锁定互斥量,但Signal和Broadcase不需要锁定互斥量。
五、原子操作
原子操作是在底层由 CPU 提供芯片级别的支持,所以绝对有效。即使在多 CPU 的系统中,仍然保证有效。
原子操作sync/atomic 的速度比其他同步工具快好几个数量级。
因为不能被中断,所以原子操作表达式 需要足够简单。
sync/atomic包中提供了几种原子操作?
加法(add)、交换(swap)、比较并交换(简称 CAS)、加载(load)、存储(store)。
支持的数据类型有:int32、int64、uint32、uint64、uintptr、unsafe.Pointer
原子操作函数需要的是 被操作值的指针,而不是这个值本身
原子加法操作的函数可以做原子减法吗?
(1) 对于int32、int64来说,直接在代表差量的第二个参数力输入负值就可以。
(2) 对于uint32、uint64 来说,可以把 负值增量 赋值给一个变量,然后强转成uint32即可。
- 比较并交换操作CAS的特点:
(1) CAS会先判断被操作变量的当前值,是否与预期的旧值相等。如果相等,就把新值赋给该变量,并返回true;否则就不赋值,直接返回false。
(2) CAS与for语句联用,可以实现简易的自旋锁。属于乐观锁,预期是共享资源状态的改变并不频繁。 而互斥量的锁属于悲观锁,预期是共享资源状态频繁改变。
即使保证了写操作都是原子操作,在读操作时,也需要保证是原子操作。 避免读到没有被修改完的值。
并发地读写互不相关的整数类型值,使用原子操作函数更优,因为速度比互斥锁快得多。
关于sync/atomic包的类型Value:
(1) sync/atomic包的类型Value,相当于一个容器,被用来“原子地”存储和加载任意的值。只有两个指针方法:Store和Load。
(2) Value类型属于结构体类型,而结构体类型属于值类型,所以不能传入nil。
(3) 存储的第一个值,决定今后能且只能存储哪一个类型的值。
(4) 使用接口的方式无效。内部是依据被存储值的实际类型来做判断的。
版权归原作者 LyndonZheng 所有, 如有侵权,请联系我们删除。