接上篇Channel案例
func GoroutineChannel() {
ch1 := make(chan int)
ch2 := make(chan int)
go func() {
for i := 0; i < 10; i++ {
ch1 <- i
time.Sleep(time.Second)
}
}()
go func() {
for j := 0; j < 10; j++ {
ch2 <- j
time.Sleep(time.Second)
}
}()
for i := 0; i < 20; i++ {
select {
case data := <-ch1:
fmt.Println("data from ch1:", data)
case data := <-ch2:
fmt.Println("data from ch2:", data)
default: //使用 default 实现非阻塞读写,不要default流程就会阻塞一直监听channel数据发送
fmt.Println("no data ready")
time.Sleep(time.Second)
}
}
fmt.Println("finish")
}
/*
GoroutineChannel的改进版,将循环取channel的值改为无限循环,且监听发送数据是否完成,
若完成,则主动退出,然后加了超时退出的判断,防止一直阻塞
*/
func GoroutineChannel2() {
ch1 := make(chan int)
ch2 := make(chan int)
chstop := make(chan bool)
wg.Add(2)
go func() {
for i := 0; i < 10; i++ {
ch1 <- i
time.Sleep(time.Second)
}
wg.Done()
}()
go func() {
for j := 0; j < 10; j++ {
ch2 <- j
time.Sleep(time.Second)
}
wg.Done()
}()
go func() {
for {
select {
case data := <-ch1:
fmt.Println("data from ch1:", data)
case data := <-ch2:
fmt.Println("data from ch2:", data)
case <-chstop:
fmt.Println("data send ok !!")
return
case <-time.After(30 * time.Second):
fmt.Println("timeout !!")
return
default: //使用 default 实现非阻塞读写,不要default,流程就会阻塞一直监听channel数据发送
fmt.Println("no data ready")
time.Sleep(time.Second)
}
}
}()
wg.Wait()
chstop <- true
fmt.Println("finish")
}
/*
GoroutineChannel2的改进,将chstop改为用Context管理协程的
*/
func GoroutineChannel3() {
ch1 := make(chan int)
ch2 := make(chan int)
ctx, cancel := context.WithCancel(context.Background())
wg.Add(2)
go func() {
for i := 0; i < 10; i++ {
ch1 <- i
time.Sleep(time.Second)
}
wg.Done()
}()
go func() {
for j := 0; j < 10; j++ {
ch2 <- j
time.Sleep(time.Second)
}
wg.Done()
}()
go func(ctx context.Context) {
for {
select {
case data := <-ch1:
fmt.Println("data from ch1:", data)
case data := <-ch2:
fmt.Println("data from ch2:", data)
case <-ctx.Done():
fmt.Println("data send ok !!")
return
case <-time.After(30 * time.Second):
fmt.Println("timeout !!")
return
default: //使用 default 实现非阻塞读写,不要default,流程就会阻塞一直监听channel数据发送
fmt.Println("no data ready")
time.Sleep(time.Second)
}
}
}(ctx)
wg.Wait()
cancel()
fmt.Println("finish")
}
一.闭包
1. 概念
闭包是指一个函数(或函数值)与其引用的外部变量(自由变量)的绑定关系。换句话说,闭包是一个函数和其相关的引用环境的组合体。
在闭包中,内部函数可以访问外部函数的变量,即使外部函数已经执行完毕,内部函数仍然可以使用外部函数的变量。这是因为闭包会在创建时将外部变量的引用保存在自己的环境中,所以即使外部函数退出后,这些变量仍然可以被内部函数访问和操作
(1).闭包是一个结构体,里面存储了一个函数和一个关联的环境;
(2).环境里包括函数内部的局部变量,也包括函数外部定义但在函数内部引用的自由变量;
(3).对于值的处理可以是值拷贝,也可以是引用;
2. 闭包的使用场景
使用闭包的意义是什么?主要就是缩小变量作用域,减少对全局变量的污染。
(1) 延迟执行:闭包可以用于实现延迟执行的功能。通过在函数内部定义一个闭包,并将其作为参数传递给其他函数或方法,在需要执行的时候再调用闭包。这在处理资源释放、错误处理等情况下非常有用。
(2) 记忆化:闭包可以用于实现记忆化功能,即缓存函数的计算结果,以避免重复计算。通过在闭包中定义一个缓存,并在每次调用闭包时检查缓存中是否存在计算结果,可以提高程序的性能。
(3) 函数工厂:闭包可以用于生成函数。通过在一个函数中定义并返回一个闭包,可以根据不同的参数生成不同的函数。这在创建具有不同配置或上下文的函数时非常有用。
(4) 事件回调:闭包可以用作事件回调函数。当某个事件发生时,闭包可以被调用,并可以访问其所在环境中的变量和状态。
(5) 并发编程:闭包可以在并发编程中用于共享变量和状态的安全访问。通过将闭包传递给goroutine,在闭包中访问共享变量时可以使用互斥锁等机制来保证线程安全。
这些只是闭包的一些常见应用场景,实际上闭包在编程中非常灵活,可以根据具体的需求进行创造性的应用。通过使用闭包,可以更好地组织和管理代码,实现更灵活、可复用和可扩展的功能。
3.闭包问题
(1).延迟绑定
闭包定义的时候并不是真正的在执行,只有当我们调用的时候才真正的执行,每次执行的时候他都会去找到他引用环境的最新值
(2).逃逸分析
逃逸分析是Go编译器的一个优化技术,用于确定一个局部变量是否逃逸到了堆上分配内存。如果一个变量逃逸到了堆上,意味着它在函数执行完后仍然可以被访问,编译器会将其分配在堆上,以保证其生命周期。
闭包中的函数常常会引用外部的变量,这些变量可能是局部变量或函数的参数。如果闭包中的函数将这些外部变量持久化(返回函数、存储到全局变量等),那么这些变量就逃逸到了堆上,因为它们在函数执行完后仍然可以被访问。逃逸分析会判断这些变量是否逃逸,并根据情况将其分配在堆上。
逃逸分析的结果会对代码的性能和内存分配产生影响。如果闭包中的变量逃逸到了堆上,可能会导致额外的内存分配和垃圾回收的开销。因此,在设计闭包时,需要注意避免不必要的变量逃逸,尽量减少对堆的依赖,以提高代码的性能和效率。
4.代码案例
(1)延迟绑定问题
func foo(x int) []func() {
var fs []func()
values := []int{1, 2, 3, 5}
for _, val := range values {
fs = append(fs, func() {
fmt.Printf("foo val = %d\n", x+val)
})
}
return fs
}
func Closure1() {
for _, f := range foo(11) { //注意此处的用法,遍历的不是函数,是函数的返回值,可以借鉴,但是注意函数的异常处理,
f() //结果输出的都是16,我们期望输出的是12,13,14,16 。这个坑我们在工作中也会经常碰到
/*
原因:
定义闭包 - 这个时候闭包只是定义,并没有使用外部的值
执行,执行的时候闭包回去寻找最新的环境引用值,有以上go for 循环底层实现分析可知,我们实际上使用的是值 v2,
v2每次都会重新覆盖。所以当闭包引用环境最新的值时,实际访问的都是v2,所以最终访问到的 val 都是5
*/
}
for i := 0; i < 10; i++ {
go func() {
fmt.Println("i_1:", i) //我电脑上的输出结果:10 10 10 10 10 5 9 10 10,实际上每次执行输出结果都不一样
}()
/*原因分析:
定义闭包
异步执行闭包
异步执行的过程寻找最新的 v2,由于是并发执行的,所以多个协程很可能访问到同一个 v2
ha := a
hv1 := 0
hn := len(ha)
v1 := hv1
v2 := nil
for ; hv1 < hn; hv1++ {
tmp := ha[hv1]
v1, v2 = hv1, tmp
...
}
go 底层每次都会把值赋给 temp, 然后把 temp 赋值给 v2,实际上我们访问的是 v2 变量
*/
}
for i := 0; i < 10; i++ {
go func() {
fmt.Println("i_2:", i) //输出结果:1 2 3 4 5 6 7 8 9,为什么延时一秒输出就正常了
}()
time.Sleep(1 * time.Second)
/*
执行过程和例子二一致,但是为什么加一秒延时就正常了,延时主要是导致协程不在并发执行了,所以每个协程获取到的都是当前引用环境的 v2
*/
}
/*
为什么以上例子会出现这些结果
1 延时绑定 闭包定义的时候并不是真正的在执行,只有当我们调用的时候才真正的执行,如例子一。每次执行的时候他都会去找到他引用环境的最新值;
2 Go 语言遍历数组和切片时会复用变量
*/
//如何解决延时绑定带来的问题
//值覆盖
for i := 0; i < 10; i++ {
i := i
go func() {
fmt.Println("i_3", i)
}()
}
//值传递
for i := 0; i < 10; i++ {
go func(i int) {
fmt.Println("i_4", i)
}(i)
}
//根据值覆盖的例子 foo1 改造后可以正常输出 12,13,14,16
for _, f1 := range foo1(11) {
f1()
}
}
func foo1(x int) []func() {
var fs []func()
values := []int{1, 2, 3, 5}
for _, val := range values {
val := val
fs = append(fs, func() {
fmt.Printf("foo1 val = %d\n", x+val)
})
}
return fs
}
(2)使用场景
//延迟执行
func ClosureDefer() {
defer func() {
fmt.Println("Deferred execution")
}()
fmt.Println("Main function")
}
//记忆化
func memoize(f func(int) int) func(int) int {
cache := make(map[int]int)
return func(n int) int {
if result, ok := cache[n]; ok {
return result
}
result := f(n)
cache[n] = result
return result
}
}
func fibonacci(n int) int {
if n <= 1 {
return n
}
return fibonacci(n-1) + fibonacci(n-2)
}
func ClosureMem() {
fib := memoize(fibonacci)
fmt.Println(fib(10))
fmt.Println(fib(5))
}
//函数工厂
func makeGreeter(name string) func() {
return func() {
fmt.Printf("Hello, %s!\n", name)
}
}
func ClosureGreeter() {
greeter := makeGreeter("John")
greeter()
}
//事件回调
type Button struct {
onClick func()
}
func (b *Button) Click() {
if b.onClick != nil {
b.onClick()
}
}
func ClosureClick() {
button := &Button{
onClick: func() {
fmt.Println("Button clicked!")
},
}
button.Click()
}
//并发编程
func ClosureWg() {
var counter int
var wg sync.WaitGroup
var mu sync.Mutex
increment := func() {
mu.Lock()
counter++
mu.Unlock()
wg.Done()
}
for i := 0; i < 10; i++ {
wg.Add(1)
go increment()
}
wg.Wait()
fmt.Println("Counter:", counter)
}
二.匿名函数
1.概念
匿名函数是一种没有函数名的函数,通常用于在函数内部定义函数,或者作为函数参数进行传递。Go 语言支持匿名函数,可作为闭包。匿名函数是一个"内联"语句或表达式。匿名函数的优越性在于可以直接使用函数内的变量,不必申明。
在Go语言中,函数是一等公民,这意味着函数可以像其他类型的值一样被操作、传递和赋值。匿名函数是一种特殊的函数,它没有固定的函数名,可以在代码中被直接定义和使用。匿名函数在Go语言中具有重要的地位,它们常用于实现闭包、函数式编程和并发编程等领域
//语法结构
func(a, b int) int {
return a + b
}
2.使用场景
将匿名函数赋值给变量、
在函数内部使用匿名函数
将匿名函数作为参数传递给其他函数
闭包的实现 匿名函数在Go语言中常用于创建闭包(Closure)
3.代码案例
func NonameFunc() {
// 定义一个匿名函数并将其赋值给变量add
add := func(a, b int) int {
return a + b
}
// 调用匿名函数
result := add(3, 5)
fmt.Println("3 + 5 =", result)
// 在函数内部使用匿名函数
multiply := func(x, y int) int {
return x * y
}
product := multiply(4, 6)
fmt.Println("4 * 6 =", product)
// 将匿名函数作为参数传递给其他函数
calculate := func(operation func(int, int) int, x, y int) int {
return operation(x, y)
}
sum := calculate(add, 2, 8)
fmt.Println("2 + 8 =", sum)
// 也可以直接在函数调用中定义匿名函数
difference := calculate(func(a, b int) int {
return a - b
}, 10, 4)
fmt.Println("10 - 4 =", difference)
//闭包
c1 := counter()
fmt.Println(c1()) // 输出 1
fmt.Println(c1()) // 输出 2
c2 := counter()
fmt.Println(c2()) // 输出 1
}
//counter() 函数返回一个匿名函数,这个匿名函数形成了闭包,持有了外部作用域中的 count 变量
func counter() func() int {
count := 0
return func() int {
count++
return count
}
}
4.匿名函数与闭包关系
闭包的实现也是一个匿名函数,一级指针在匿名函数里面指向“func1.f”, 在闭包中,指向闭包返回对象。闭包返回的包装对象是一个复合结构,里面包含匿名函数的地址,以及环境变量的地址。
- 被闭包捕获的变量称为“自由变量”,在匿名函数实例未消亡时共享同个内存地址
- 匿名函数及其“捕获”的自由变量被称为闭包
- 同一个匿名函数可构造多个实例,每个实例内的 自由变量 地址不同
- 匿名函数内部的局部变量在每次执行匿名函数时地址都是变换的
三.锁
在 Go 语言中,信道的地位非常高,它是 first class 级别的,面对并发问题,我们始终应该优先考虑使用信道,如果通过信道解决不了的,不得不使用共享内存(共享变量)来实现并发编程的,那 Golang 中的锁机制,就是你绕不过的知识点了。
在 Golang 里有专门的方法来实现锁
这个sync
包有两个很重要的锁类型
一个叫 Mutex
, 利用它可以实现互斥锁。
一个叫 RWMutex
,利用它可以实现读写锁。
1. 互斥锁 :Mutex
使用互斥锁(Mutex,全称 mutual exclusion)是为了来保护一个资源不会因为并发操作而引起冲突导致数据不准确。
2. 读写锁:RWMutex
Mutex 是最简单的一种锁类型,他提供了一个傻瓜式的操作,加锁解锁加锁解锁,让你不需要再考虑其他的。
Mutex在大量并发的情况下,会造成锁等待,对性能的影响比较大。
如果某个读操作的协程加了锁,其他的协程没必要处于等待状态,可以并发地访问共享变量,这样能让读操作并行,提高读性能。
RWMutex 里提供了两种锁,每种锁分别对应两个方法,为了避免死锁,两个方法应成对出现,必要时请使用 defer。
- 读锁:调用 RLock 方法开启锁,调用 RUnlock 释放锁
- 写锁:调用 Lock 方法开启锁,调用 Unlock 释放锁(和 Mutex类似)
主要遵循以下规则 :
- 读写锁的读锁可以重入,在已经有读锁的情况下,可以任意加读锁。
- 在读锁没有全部解锁的情况下,写操作会阻塞直到所有读锁解锁。
- 写锁定的情况下,其他协程的读写都会被阻塞,直到写锁解锁。
Go语言的读写锁方法主要有下面这种
- Lock/Unlock:针对写操作。
不管锁是被reader还是writer持有,这个Lock方法会一直阻塞,Unlock用来释放锁的方法 - RLock/RUnlock:针对读操作
当锁被reader所有的时候,RLock会直接返回,当锁已经被writer所有,RLock会一直阻塞,直到能获取锁,否则就直接返回,RUnlock用来释放锁的方法
3.总结
理解和正确使用互斥锁(sync.Mutex
)与读写锁(sync.RWMutex
)是编写并发安全Go程序的基础。牢记以下要点:
- 互斥锁确保同一时刻只有一个
goroutine
访问资源,适用于写多或读写均衡的场景。 - 读写锁优化了读取操作,允许多个读取者同时访问资源,适用于读多写少的场景。
- 避免忘记解锁、重复解锁、混淆读写锁与互斥锁,以及试图升级或降级锁。
- 利用
defer
语句确保解锁操作的正确执行,提高代码可读性和健壮性。
通过遵循这些原则,您将在Go并发编程中有效地利用锁机制,构建安全、高效的并发应用程序。
4. 代码案例
//互斥锁
func Mutex() {
MutexSub1()
MutexSub2()
}
func MutexSub1() {
var count = 0
var wg sync.WaitGroup
//十个协程数量
n := 10
wg.Add(n)
for i := 0; i < n; i++ {
go func() {
defer wg.Done()
//1万叠加
for j := 0; j < 10000; j++ {
count++
}
}()
}
wg.Wait()
fmt.Println("sub1:", count) //运行结果:59420(不固定),正确的结果应该是100000,这里出现了并发写入更新错误的情况
}
func MutexSub2() {
var count = 0
var wg sync.WaitGroup
var mu sync.Mutex
//十个协程数量
n := 10
wg.Add(n)
for i := 0; i < n; i++ {
go func() {
defer wg.Done()
//1万叠加
for j := 0; j < 10000; j++ {
mu.Lock()
count++
mu.Unlock()
}
}()
}
wg.Wait()
fmt.Println("sub2:", count) //运行结果为正确的:100000
}
//读写锁
func RwMutex() {
lock := &sync.RWMutex{}
lock.Lock()
for i := 0; i < 4; i++ {
go func(i int) {
fmt.Printf("第 %d 个协程准备开始... \n", i)
lock.RLock()
fmt.Printf("第 %d 个协程获得读锁, sleep 1s 后,释放锁\n", i)
time.Sleep(time.Second)
lock.RUnlock()
}(i)
}
time.Sleep(time.Second * 2)
fmt.Println("准备释放写锁,读锁不再阻塞")
// 写锁一释放,读锁就自由了
lock.Unlock()
// 由于会等到读锁全部释放,才能获得写锁
// 因为这里一定会在上面 4 个协程全部完成才能往下走
lock.Lock()
fmt.Println("程序退出...")
lock.Unlock()
}
四.原子操作包(sync/atomic)
1.概念
Mutux,WaitGroup 等,到最近的 Map、Pool,我们已经了解了很多并发原语,绝大部分并发场景的问题都可以通过这些原语解决。
但是,使用这些并发原语并不是一件性价比很高的事情。比如,data race 场景中,锁常常导致系统性能下降。
原子操作能帮助我们进行更底层的优化,在实现相同效果的同时大大减少资源的消耗。
在Go语言的并发编程中,sync/atomic
包提供了对整型值和指针进行原子操作的支持,确保这些操作在多线程环境中不会受到数据竞争的影响。
sync/atomic
包主要包含以下几种原子操作:
- 原子整数操作:如
AddInt32
、CompareAndSwapInt32
等,用于对32位或64位整型变量进行原子加减、交换、加载、存储等操作。 - 原子指针操作:如
SwapPointer
、StorePointer
等,用于对指针进行原子交换、存储等操作。 - 原子标量函数:如
LoadUint32
、StoreUint32
等,提供对各种宽度(32位、64位)和类型的标量值进行原子加载和存储。
2.操作方法
原子操作主要是两类:
修改存储:就是在原来值的基础上改动;。
加载存储:加载存储就是读写。
atomic 提供了 AddXXX、CompareAndSwapXXX、SwapXXX、LoadXXX、StoreXXX 等方法。
由于 Go 暂时还不支持泛型,所以很多方法的实现都很啰嗦,比如 AddXXX 方法,针对 int32、int64、uint32 基础类型,每个类型都有相应的实现。等 Go 支持泛型之后,相信 atomic 的 API 就会清爽很多。
需要注意的是,atomic 的操作对象是地址,所以传参的时候,需要传变量的地址,不能传变量的值。
Add 方法
Add 方法很好理解,对 addr 指向的值加上 delta。如果将 delta 设置成负值,加法就变成了减法。
Add 方法的签名有如下5个:
func AddInt32(addr *int32, delta int32) (new int32)
func AddInt64(addr *int64, delta int64) (new int64)
func AddUint32(addr *uint32, delta uint32) (new uint32)
func AddUint64(addr *uint64, delta uint64) (new uint64)
func AddUintptr(addr *uintptr, delta uintptr) (new uintptr)
这里有个细节,像 AddUint32 针对无符号整型的操作,它实现减法的操作略微复杂一些,可以利用计算机补码的规则,把减法变成加法。
以 AddUint32 为例:
AddUint32(&x, ^uint32(c-1))
这样就实现了 x-c
的效果。当然,如果觉得这样麻烦,用下面的 CAS 方法也可以。
CAS 方法
CAS 的全称是 CompareAndSwap,它支持的数据类型和方法如下:
func CompareAndSwapInt32(addr *int32, old, new int32) (swapped bool)
func CompareAndSwapInt64(addr *int64, old, new int64) (swapped bool)
func CompareAndSwapPointer(addr *unsafe.Pointer, old, new unsafe.Pointer) (swapped bool)
func CompareAndSwapUint32(addr *uint32, old, new uint32) (swapped bool)
func CompareAndSwapUint64(addr *uint64, old, new uint64) (swapped bool)
func CompareAndSwapUintptr(addr *uintptr, old, new uintptr) (swapped bool)
这个方法会比较当前 addr 地址对应值是不是等于 old,等于的话就更新成 new,并返回 true,不等于的话返回 false。
CAS 本身并未实现失败的后的处理机制,只不过我们最常用的处理方式是重试而已。
Swap 方法
如果不需要比较,直接交换的话,也可以用 Swap 方法。它支持的数据类型和方法如下:
func SwapInt32(addr *int32, new int32) (old int32)
func SwapInt64(addr *int64, new int64) (old int64)
func SwapPointer(addr *unsafe.Pointer, new unsafe.Pointer) (old unsafe.Pointer)
func SwapUint32(addr *uint32, new uint32) (old uint32)
func SwapUint64(addr *uint64, new uint64) (old uint64)
func SwapUintptr(addr *uintptr, new uintptr) (old uintptr)
和下文的 Store 不一样的是,Swap 会返回旧值,因此被叫做置换操作。
Load 方法
Load 方法会取出 addr 地址中的值,它支持的数据类型和方法如下:
func LoadInt32(addr *int32) (val int32)
func LoadInt64(addr *int64) (val int64)
func LoadPointer(addr *unsafe.Pointer) (val unsafe.Pointer)
func LoadUint32(addr *uint32) (val uint32)
func LoadUint64(addr *uint64) (val uint64)
func LoadUintptr(addr *uintptr) (val uintptr)
即使在多处理器、多核、有 CPU cache 的情况下,Load 方法能保证数据的读一致性。
Store 方法
Store 方法会将一个值存到指定的 addr 地址中去它支持的数据类型和方法如下:
func StoreInt32(addr *int32, val int32)
func StoreInt64(addr *int64, val int64)
func StorePointer(addr *unsafe.Pointer, val unsafe.Pointer)
func StoreUint32(addr *uint32, val uint32)
func StoreUint64(addr *uint64, val uint64)
func StoreUintptr(addr *uintptr, val uintptr)
其它协程通过 Load 读取数据,不会看到存取了一半的值。
Value 类型
上面的几种方法只支持基本的几种类型,因此,atomic 还提供了一个 Value 类型,它可以实现对任意类型(结构体)原子的存取操作。
Value 类型的定义以及支持的方法如下:
// A Value provides an atomic load and store of a consistently typed value.
// The zero value for a Value returns nil from Load.
// Once Store has been called, a Value must not be copied.
//
// A Value must not be copied after first use.
type Value struct {
v interface{}
}
func (v *Value) Load() (x interface{})
func (v *Value) Store(x interface{})
相比于上面的 StoreXXX 和 LoadXXX,value的操作效率会低一些,不过胜在简单易用。