语言基础和go的同步包
简介
本文回顾go语言基础和同步包。
1, 25个关键字。
-
并且没有解析符号表。
-
const 常量是数字,首字母大小决定结构体属性的可见性。
-
没有子类型继承。
-
隐式的接口,implements 声明
-
内存总是初始化零值
-
go1 规范:
interface 水平组合, 满足依赖抽象,里氏替换,接口隔离 type embedding 垂直组合, 通过方法名 匹配和调用函数。 通过 例程 goroutine + channel 组合实现类似 unix pipe 的功能。 并发有关代码结构。 并行有关执行的结构。 解决了性能问题,单靠cpu主频提高以提高性能不总是有效的。 大型程序中,操作系统调度切换线程代价不小。 所谓自带电池是指,内建的标准库丰富。
-
使用go的挑战
1 包管理 2 相似的语言特征 3 泛型支持 4 客户端GUI支持 5 错误处理 6 调试和库支持
1.1 go的标准项目结构
原生编程思维 ---> idiomatic go 内建工具链,go语言语法
标准库
三方库、工具
项目结构,go 风格 ---> api
lib
src --> cmd ---> cgo, gc,gofmt,yacc ...
pkg ---> fmt/, go/, io/, math/, syscall/,unsafe/,...
vandor...
go项目推荐 module-ware风格 ---> go.mod
go.num
lib.go
lib1
lib2
缓存失效和命名的问题
命名: ---> var 变量声明聚类,同类型的相同前后缀
const 常量命名,整合了c的风格 ---> 宏定义常量, 枚举常量
const 只读变量,为类型安全的。
无类型常量
const SearchStart = 1
有类型常量
const SearchEnd int = 10
枚举类型 iota是 Go的一个预定义标识符,它表示const 声明块,包括单行声明的每个常量所处位置在块中的偏移量(从0开始)。
零值可用类型
某些数据类型可以支持零值可用,但是功能受限
* 切片 零值切片可添加元素,但是不能使用下标。
* 互斥锁和池 sync.Mutex sync.Pool
有大并发需求的创建,使用读写锁。 RWMutex
* io 缓冲 bytes.Buffer 避免值复制错误
创建切片的cap参数
St := make([]int, 0)
size := 10
Se := make([]int, 0, size)
使用cap容量规定切片最大有多少元素,可以预知切片最大容量,提高 40%的性能。
函数的可变参数, 如下 Call函数有可变参数类型,在函数最后可以使用,不传或传多个。
func Call(name, ...String) {...}
-
错误处理
1 构造错误值对象 fmt.Errorf(…), errors.New(…)
2 透明错误处理,实现低耦合 if err != nil {…}
3 哨兵处理方式if err != nil { switch err.Error() { case "bufio:negative...": ... case .... } }
4 错误类型检视
switch err := err.(type) { case * unmarshal.TypeError{ ... } }
5 错误行为检查
将错误封装到结构体 type OpError struct { ... err error } type temp interface { Temp() } func (eop *OpError) Temp() bool { if ne, ok := eop.Err.(* os.Syscall.Error); ok { t, ok := ne.Err.(temp) return ok && t.Temp } }
6 panic 恐慌的使用场景
1 充当断言 2 简化错误处理 3 recover 捕获 panic, 防止协程退出 由于在go中,多例程时,无论哪个例程退出,整个go程序都将失败。 panic 常常都被用于程序栈跟踪。
1.2 sync 包
sync同步包 在 src/sync/ 路径
在其中有这样的提示:
不应该复制哪些包含了此包中类型的值。
禁止复制首次使用后的Mutex
禁止复制使用后的RWMutex
禁止复制使用后的Cond
例子1: 复制互斥锁
type mutn struct {
n int
sync.Mutex
}
func DoMutn() {
f := mutn{n: 19}
go func(f mutn) {
for {
log.Println("goto: try to lock mutn")
f.Lock()
log.Println("goto: lock mutn ok.")
time.Sleep(3 * time.Second)
f.Unlock()
log.Println("goto:unlock ok")
}
}(f)
f.Lock()
log.Println("goto:lock mutn ok.")
Mutex 互斥锁首次使用后复制其值
go func(f mutn) {
for {
log.Println("gr3: try to lock...")
f.Lock()
log.Println("gr3: lock mutn ok.")
time.Sleep(5 * time.Second)
f.Unlock()
log.Println("gr3:unlock ok")
}
}(f)
time.Sleep(20 * time.Second)
f.Unlock()
log.Println("g1:unlock ok")
}
func main() {
DoMutn()
}
我们在示例中创建两个goroutine,g2和g3,示例运行的结果显示: gr3阻塞在加锁操作。
而g2按预期运行,g2和g3的区别在于g2是互斥锁首次使用之前创建的。
而g3则是在互斥锁执行完加锁操作并处于锁定状态时创建的。
并且程序在创建g3时复制了 mutn实例。 该实例包括了sync.Mutex实例。
在标准库Mutex的定义非常简单,它有两个字段 state,sema组成
type Mutex struct {
state int32
sema uint32
}
这两个字段表示
state 表示当前互斥锁状态。
sema 用于控制锁状态信号量。
对mutex实例的复制即是对两个整型字段的复制。 在初始状态,Mutex实例处于 Unlocked状态,state和sema都为0.
g2复制了处于初始化的Mutex实例,副本的state和sema为0, 这与g2自定义一个新的Mutex实例无异,这决定了g2后续可以按预期运行。
后续主程序调用Lock方法,Mutex 实例变成 Locked状态,state字段值为 sync.mutex-Locked
此后g3创建时恰恰复制了处于 Locked状态的实例。
实例副本state字段值也为 sync.mutexLocked ,因此g3在对其实例副本调用Lock将导致进入阻塞
— 也就是死锁 因为没有任何其他计划调用该副本的Unlock方法,Go不支持递归锁—
那些sync包中类型的实例在首次使用后被复制得到的副本,一旦再被使用将导致不可预期结果,为此在使用sync包的类型时,
推荐通过闭包方式或传递类型实例(或包裹该类型的类型实例)的地址或指针进行,这是sync包最需要注意的。
互斥锁 sync.Mutex 临界区同步原语首选,常被用来对结构体对象内部状态,缓存进行保护。 使用最为广泛。
它通常被用以保护结构体内部状态,缓存,是广泛的临界区同步原语。
读写锁 RWMutex 有大并发需求的创建,使用读写锁。 RWMutex
读写锁适合具有一定并发量,并且读取操作明显大于写操作的场景。
例子2:
package main
import (
"sync"
"testing"
)
var (
临界区需要保护的数据
dataOne = 0
dataTwo = 1
mutexOne sync.Mutex
mutexTwo sync.RWMutex
)
互斥锁性能 读取
func BenchmarkReadSyncByMutex(b *testing.B) {
b.RunParallel(func(pb *testing.PB) {
for pb.Next() {
mutexOne.Lock()
_ = dataOne
mutexOne.Unlock()
}
})
}
互斥锁性能 写入
func BenchmarkWriteSyncByMutex(b *testing.B) {
b.RunParallel(func(pb *testing.PB) {
for pb.Next() {
mutexOne.Lock()
dataOne += 1
mutexOne.Unlock()
}
})
}
读取性能评估
func BenchmarkReadSyncByRWMutex(b *testing.B) {
b.RunParallel(func(pb *testing.PB) {
for pb.Next() {
mutexTwo.Lock()
_ = dataTwo
mutexTwo.Unlock()
}
})
}
写性能评估
func BenchmarkWriteSyncByRWMutex(b *testing.B) {
b.RunParallel(func(pb *testing.PB) {
for pb.Next() {
mutexTwo.Lock()
dataTwo += 1
mutexTwo.Unlock()
}
})
}
执行:
go test -v -count 2 -bench . mutex_rw_bench_test.go -cpu 2,4,8,32,128 >bm.txt
goarch: amd64
cpu: AMD Ryzen 5 3500U with Radeon Vega Mobile Gfx
BenchmarkReadSyncByMutex
BenchmarkReadSyncByMutex-2 30831019 40.23 ns/op
BenchmarkReadSyncByMutex-2 32428663 42.62 ns/op
BenchmarkReadSyncByMutex-4 10713606 114.1 ns/op
BenchmarkReadSyncByMutex-4 10344114 98.16 ns/op
BenchmarkReadSyncByMutex-8 10293854 116.5 ns/op
BenchmarkReadSyncByMutex-8 10168749 116.9 ns/op
BenchmarkReadSyncByMutex-32 11110328 111.3 ns/op
BenchmarkReadSyncByMutex-32 10753728 108.3 ns/op
BenchmarkReadSyncByMutex-128 12562038 98.00 ns/op
BenchmarkReadSyncByMutex-128 12499010 96.89 ns/op
BenchmarkWriteSyncByMutex
BenchmarkWriteSyncByMutex-2 17350693 67.81 ns/op
BenchmarkWriteSyncByMutex-2 15188412 66.77 ns/op
BenchmarkWriteSyncByMutex-4 9374296 125.0 ns/op
BenchmarkWriteSyncByMutex-4 10168714 126.8 ns/op
BenchmarkWriteSyncByMutex-8 9916609 119.1 ns/op
BenchmarkWriteSyncByMutex-8 9755517 121.1 ns/op
BenchmarkWriteSyncByMutex-32 10713538 113.9 ns/op
BenchmarkWriteSyncByMutex-32 10568701 113.5 ns/op
BenchmarkWriteSyncByMutex-128 11649591 102.3 ns/op
BenchmarkWriteSyncByMutex-128 11973096 102.5 ns/op
BenchmarkReadSyncByRWMutex
BenchmarkReadSyncByRWMutex-2 13524128 102.7 ns/op
BenchmarkReadSyncByRWMutex-2 11999124 101.4 ns/op
BenchmarkReadSyncByRWMutex-4 8391038 145.8 ns/op
BenchmarkReadSyncByRWMutex-4 14412699 126.1 ns/op
BenchmarkReadSyncByRWMutex-8 10525567 116.3 ns/op
BenchmarkReadSyncByRWMutex-8 10255752 116.4 ns/op
BenchmarkReadSyncByRWMutex-32 10255778 117.3 ns/op
BenchmarkReadSyncByRWMutex-32 10208638 117.9 ns/op
BenchmarkReadSyncByRWMutex-128 10810089 111.0 ns/op
BenchmarkReadSyncByRWMutex-128 11110348 108.1 ns/op
BenchmarkWriteSyncByRWMutex
BenchmarkWriteSyncByRWMutex-2 12499010 91.11 ns/op
BenchmarkWriteSyncByRWMutex-2 11999124 99.52 ns/op
BenchmarkWriteSyncByRWMutex-4 7842598 147.7 ns/op
BenchmarkWriteSyncByRWMutex-4 7946450 151.0 ns/op
BenchmarkWriteSyncByRWMutex-8 10210080 118.1 ns/op
BenchmarkWriteSyncByRWMutex-8 10168724 115.7 ns/op
BenchmarkWriteSyncByRWMutex-32 9835380 119.9 ns/op
BenchmarkWriteSyncByRWMutex-32 10339772 117.5 ns/op
BenchmarkWriteSyncByRWMutex-128 10908296 109.5 ns/op
BenchmarkWriteSyncByRWMutex-128 10810030 109.9 ns/op
PASS
简单分析:
1 在小并发量时,互斥锁性能更好,并发量增大,互斥锁竞争激烈,导致加锁和解锁性能下降,
但是最后也恒定在最好记录的2倍左右。
2 读写锁的读锁性能并未随着并发量增大而性能下降,始终在恒定值.
3 并发量较大时,读写锁的写锁性能比互斥锁,读写锁的读锁都差,并且随着并发量增大,写锁性能有继续下降趋势。
多个例程goroutine可以同时持有读锁,从而减少在锁竞争等待的时间。
而互斥锁即便为读请求,同一时刻也只能有一个例程持有锁,其他goroutine被阻塞在加锁操作等待被调度。
由于处于for循环测试中,需要注意的是,不能在 unlock时使用 defer,
b.RunParallel(func(pb *testing.PB) {
for pb.Next() {
mutexTwo.Lock()
dataTwo += 1
defer mutexTwo.Unlock()
}
})
如此在并发执行时,函数不会退出,defer得不到执行,将导致全部死锁。
-
条件变量 sync.Cond
同步原语之一,用于避免轮询。 本质是一个容器,存放了一个或一组等待某条件的一些 协程 goroutine 当条件成立时,这些协程 goroutine将被唤醒去执行。
例子3: 条件变量的实现
import (
"fmt"
"sync"
"time"
)
使用sync.Cond 实例初始化
type signal struct{}
var ready bool
func worker(i int) {
fmt.Printf("worker %d: is working...\n", i)
time.Sleep(1 * time.Second)
fmt.Printf("worker %d:worker done\n", i)
}
func spawnGroup(f func(i int), num int, mu *sync.Mutex) <-chan signal {
c := make(chan signal)
var wg sync.WaitGroup
for i := 0; i < num; i++ {
wg.Add(1)
go func(i int) {
for {
mu.Lock()
if !ready {
mu.Unlock()
time.Sleep(100 * time.Microsecond)
continue
}
mu.Unlock()
fmt.Printf("worker %d: start to work...\n", i)
f(i)
wg.Done()
return
}
}(i + 1)
}
go func() {
wg.Wait()
c <- signal(struct{}{})
}()
return c
}
模拟ready准备工作
func DoWorkers() {
fmt.Println("start a group of workers...")
mu := &sync.Mutex{}
c := spawnGroup(worker, 5, mu)
time.Sleep(5 * time.Second)
fmt.Println("the group of workers start to work...")
mu.Lock()
ready = true
mu.Unlock()
<-c
fmt.Println("the group of workers work done!")
}
func main() {
DoWorkers()
}
执行例子3:
go run .\cond_case.go
start a group of workers...
the group of workers start to work...
worker 5: start to work...
worker 5: is working...
worker 1: start to work...
worker 1: is working...
worker 2: start to work...
worker 2: is working...
worker 4: start to work...
worker 4: is working...
worker 3: start to work...
worker 3: is working...
worker 3:worker done
worker 5:worker done
worker 2:worker done
worker 1:worker done
worker 4:worker done
the group of workers work done!
我们看到sync.Cond 实例的初始化需要一个满足实现了sync.Locker 接口的类型实例,
通常我们使用sync.Mutex.
条件变量需要这个互斥锁同步临界区,保护用作条件的数据。
各个等待条件成立的例程 goroutine 在加锁后判断条件是否成立,如果不成立,则调用 sync.Cond 的Wait方法进入等待。
Wait方法在goroutine挂起前进行Unlock操作。
在执行函数,例程将ready置为true并调用sync.Cond 的Broadcast方法,各个阻塞的例程将被唤醒并从Wait方法返回。
在Wait方法返回前,Wait方法再次加锁让goroutine进入临界区。 接下来例程将再次对条件数据进行判定。
如果条件成立,则解锁进入下一个阶段,如果不成立,那么再次调用Wait方法挂起等待。
-
单例支持 sync.Once
在go原生只被执行一次且goroutine安全的函数只有每个的包的init函数。 sync.Once 提供了另一种更为灵活的机制,可以保证任意一个函数在程序运行期间只运行一次。 这经常被用于初始化和资源清理过程,以避免重复执行初始化或资源清理操作。
例子4: 单例的实现
package main
import (
"log"
"os"
"sync"
"time"
)
type Foo struct{}
var (
once sync.Once
instance *Foo
logger = log.New(os.Stdout, "info-", 18)
)
func GetInstance(id int) *Foo {
defer func() {
if e := recover(); e != nil {
logger.Printf("goroutine-%d:caught a panic:%s", id, e)
}
}()
logger.Printf("goroutine-%d:enter GetInstance\n", id)
once.Do(func() {
instance = &Foo{}
time.Sleep(3 * time.Second)
logger.Printf("goroutine-%d:the addr of instance is %p \n", id, instance)
panic("once.Do func panic")
})
return instance
}
func DoMains() {
var wg sync.WaitGroup
for i := 0; i < 5; i++ {
wg.Add(1)
go func(i int) {
inst := GetInstance(i)
logger.Printf("goroutine-%d, the addr of instance returned is %p\n", i, inst)
wg.Done()
}(i + 1)
}
time.Sleep(5 * time.Second)
inst := GetInstance(0)
logger.Printf("goroutine-0:the addr of instance is %p\n", inst)
wg.Wait()
logger.Printf("all goroutines exit.\n")
}
func main() {
DoMains()
}
执行结果:
info-10:48:39 once_pkg.go:25: goroutine-2:enter GetInstance
info-10:48:39 once_pkg.go:25: goroutine-5:enter GetInstance
info-10:48:39 once_pkg.go:25: goroutine-4:enter GetInstance
info-10:48:39 once_pkg.go:25: goroutine-3:enter GetInstance
info-10:48:39 once_pkg.go:25: goroutine-1:enter GetInstance
info-10:48:42 once_pkg.go:29: goroutine-2:the addr of instance is 0x1033f38
info-10:48:42 once_pkg.go:21: goroutine-2:caught a panic:once.Do func panic
info-10:48:42 once_pkg.go:41: goroutine-2, the addr of instance returned is 0x0
info-10:48:42 once_pkg.go:41: goroutine-5, the addr of instance returned is 0x1033f38
info-10:48:42 once_pkg.go:41: goroutine-4, the addr of instance returned is 0x1033f38
info-10:48:42 once_pkg.go:41: goroutine-3, the addr of instance returned is 0x1033f38
info-10:48:42 once_pkg.go:41: goroutine-1, the addr of instance returned is 0x1033f38
info-10:48:44 once_pkg.go:25: goroutine-0:enter GetInstance
info-10:48:44 once_pkg.go:47: goroutine-0:the addr of instance is 0x1033f38
info-10:48:44 once_pkg.go:49: all goroutines exit.
once.Do 将等待f执行完毕后返回,这期间其他执行once.Do函数的例程 将会阻塞等待。
如上 goroutine 2~5 的 Do函数返回后,后续的goroutine在执行Do函数将不再执行f并立即返回。
如上面的 gouroutine-0 即便在函数f中出现 panic,sync.Once 原语也认为once.Do 执行完毕,后续对once.Do调用将不再执行f。
- sync.Pool 降低垃圾回收成本
官方介绍:
一个同步池是一组临时对象,可能是单独保存和检索的。
在池中存储的任何项目,都可以随时被删除,并且不会有删除通知。
如果池只有唯一的引用,那么该项目可能被剥离。
池可以被多个例程同时使用,并且是安全的。
池的目标是那些已经分配缓存但暂时没有使用的项目,以供再次使用。这将减轻GC压力。
这个特点使得它容易被用于建立高效,线程安全的列表。
但是它并不适合全部场景的自由大小的列表。 free lists
适当使用池管理一组临时项目,并务必在并发独立的包的独立客户端使用。
池提供了一种方式,支持跨多个客户端的摊销分配
使用池子的一个良好例子是 fmt 包,它用以维护一个动态大小的临时输出缓冲区。
当有许多goroutine例程在打印时,池子将在有负载时放大,在静止下缩小。
另一方面,一个不限制大小的列表作为 短周期对象不适合被用作池子。
该场景的开销不会被分摊。
短周期对象使用自己的自由列表将更高效。
池在使用后,不能被复制。
在Go内存模型术语中, Put(x) 被调用 synchronizes before
一个调用为获取返回同样的值 x。
类似地,一个调用到新的返回值 x synchronizes before
另一个调用为获取相同的值x。
type Pool struct {
noCopy noCopy
local unsafe.Pointer
localSize uintptr
victim unsafe.Pointer
victimSize uintptr
New func() any
}
属性 local 分配固定尺寸的 池子,实际类型为 [P]poolLocal
属性 localSize 本地数组大小
属性 victim 从上一个循环来的local
属性 victimSize victims数组的大小
func() 可选择指定何时将返回 nil 时,生成值功能,它可能不会随时更改。
性能评估代码:
var (
p sync.Pool
)
基础功能 依次 放入,获取 一个
func BenchmarkPool(b *testing.B) {
b.RunParallel(func(pb *testing.PB) {
for pb.Next() {
p.Put(1)
p.Get()
}
})
}
连续放入100个,再连续拿取100个
func BenchmarkPoolOverflow(b *testing.B) {
b.RunParallel(func(pb *testing.PB) {
for pb.Next() {
for b := 0; b < 100; b++ {
p.Put(1)
}
for b := 0; b < 100; b++ {
p.Get()
}
}
})
}
模拟饥饿对象,以强制从其他池子拿取对象
func BenchmarkPoolStarvation(b *testing.B) {
var p sync.Pool
count := 100
将放入的对象数量减少33%
这将创建pool对象饥饿
并且强制 P-local 存储器 从其他池子Ps拿取对象。
countStarved := count - int(float32(count)*0.33)
b.RunParallel(func(pb *testing.PB) {
for pb.Next() {
for b := 0; b < countStarved; b++ {
p.Put(1)
}
for b := 0; b < count; b++ {
p.Get()
}
}
})
}
性能执行:
go test -v -count 2 -bench . .\pool_sync_test.go -cpu 2,4,8,32,128 >bmpool.txt
goarch: amd64
cpu: AMD Ryzen 5 3500U with Radeon Vega Mobile Gfx
BenchmarkPool
BenchmarkPool-2 92275749 12.69 ns/op
BenchmarkPool-2 100000000 12.57 ns/op
BenchmarkPool-4 205023169 5.613 ns/op
BenchmarkPool-4 228077668 6.074 ns/op
BenchmarkPool-8 321266732 3.493 ns/op
BenchmarkPool-8 345006420 3.483 ns/op
BenchmarkPool-32 349300358 3.690 ns/op
BenchmarkPool-32 310861339 3.449 ns/op
BenchmarkPool-128 339489630 3.491 ns/op
BenchmarkPool-128 360954796 3.460 ns/op
BenchmarkPoolOverflow
BenchmarkPoolOverflow-2 659786 1543 ns/op
BenchmarkPoolOverflow-2 749961 1728 ns/op
BenchmarkPoolOverflow-4 1295523 919.3 ns/op
BenchmarkPoolOverflow-4 1296963 933.8 ns/op
BenchmarkPoolOverflow-8 2085678 575.9 ns/op
BenchmarkPoolOverflow-8 2098604 583.7 ns/op
BenchmarkPoolOverflow-32 2099030 570.7 ns/op
BenchmarkPoolOverflow-32 2083220 571.3 ns/op
BenchmarkPoolOverflow-128 1942476 605.2 ns/op
BenchmarkPoolOverflow-128 2034183 604.0 ns/op
PASS
ok command-line-arguments 33.053s
- 点赞
- 收藏
- 关注作者
评论(0)