当前位置: 首页 > news >正文

Go语言 并发安全sync

1. 并发概述

进程与线程

        谈到并发或者并行一个绕不开的话题就是进程和线程,弄清楚进程与线程的本质是并发编程的前提,那么究竟什么是进程,什么是线程呢?
        可以这样理解:

  • 进程就是运行着的程序,它是程序在操作系统的一次执行过程,是一个程序的动态概念,进程是操作系统分配资源的基本单位
  • 线程可以理解为一个进程的执行实体,它是比进程粒度更小的执行单元,也是真正运行在cpu上的执行单元,线程是CPU调度资源的基本单位
    进程中可以包含多个线程,需要记住进程和线程一个是操作系统分配资源的基本单位(进程),一个是操作系统调度资源的基本单位(线程)

协程

        协程可以理解为用户态线程,是更微量级的线程。区别于线程,协程的调度在用户态进行,不需要切换到内核态,所以不由操作系统参与,由用户自己控制。在一些支持协程高级语言中,往往这些语言都实现了自己的协程调度器,比如go语言就有自己的协程调度器,这个会在后面专门讲协程调度原理的时候讲。

  • 协程有独立的栈空间,但是共享堆空间。
  • 一个进程上可以跑多个线程,一个线程上可以跑多个协程

并发与并行 

        很多时候大家对于并行和并发的概念还比较模糊,其实只需要根据一点来判断即可,能不能同时运行。两个任务能同时运行就是并行,不能同时运行,而是每个任务执行一小段,交叉执行,这种模式就是并发。 

        要注意并行的话一定要有多个核的支持。因为只有一个cpu的话,同一时间只能跑一个任务,两个任务,每次只执行一小段,这样交叉的执行,就是并发模式,并发模式在单核cpu上是可以完成的

sync包

sync.WaitGroup

        在前面很多goroutine的示例中,我们都是通过time.Sleep()方法让主goroutine等待一段时间以便子gortoutine能够执行完打印结果,显然这不是一个很好的办法,因为我们不知道所有的子gortoutine要多久才能执行完,不能确切的知道需要等待多久。那要怎么处理呢?

使用channel实现等待

看下面例子:

package mainimport ("fmt"
)func main() {ch := make(chan struct{}, 10)for i := 0; i < 10; i++ {go func(i int) {fmt.Printf("num:%d\n",i)ch <- struct{}{}}(i)}for i := 0; i < 10; i++ {<-ch}fmt.Println("end")
}

运行结果:

num:0
num:2
num:1
num:4
num:6
num:7
num:5
num:8
num:9
num:3
end

        我们在每个goroutine中,向管道里发送一条数据,这样我们在程序最后,通过for循环将管道里的数据全部取出,直到数据全部取出完毕才能继续后面的逻辑,这样就可以实现等待各个goroutine执行完。
        但是,这样使用channel显得并不优雅。其次,我们得根据具体循环的次数,来创建管道的大小,假设次数非常的多,则需要申请同样数量大小缓存的管道出来,对内存也是不小的开销。 

使用WaitGroup实现等待

这里我们可以用sync包下的WaitGroup来实现,Go语言中可以使用sync.WaitGroup来实现并发任务的同步以及协程任务等待。
sync.WaitGroup是一个对象,里面维护者一个计数器,并且通过三个方法来配合使用:

  • (wg * WaitGroup) Add(delta int) 计数器加delta
  • (wg *WaitGroup) Done() 计数器减1
  • (wg *WaitGroup) Wait() 会阻塞代码的运行,直至计数器减为0

先看示例: 

package mainimport ("fmt""sync"
)var wg sync.WaitGroupfunc myGoroutine() {defer wg.Done()fmt.Println("myGoroutine!")
}func main() {wg.Add(10)for i := 0; i < 10; i++ {go myGoroutine()}wg.Wait()fmt.Println("end!!!")
}

运行结果: 

myGoroutine!
myGoroutine!
myGoroutine!
myGoroutine!
myGoroutine!
myGoroutine!
myGoroutine!
myGoroutine!
myGoroutine!
myGoroutine!
end!!!

        程序首先把wg的计数设置为10,每个for循环运行完毕都把计数器减1,main函数中执行到wg.Wait()会一直阻塞,直到wg的计数器为零。最后打印了10个myGoroutine!,是所有子goroutine任务结束后主goroutine才退出。
        注意:sync.WaitGroup对象的计数器不能为负数,否则会panic,在使用的过程中,我们需要保证add()的参数值,以及执行完Done()之后计数器大于等于零 

sync.Once

        在我们写项目的时候,程序中有很多的逻辑只需要执行一次,最典型的就是项目工程里配置文件的加载,我们只需要加载一次即可,让配置保存在内存中,下次使用的时候直接使用内存中的配置数据即可。这里就要用到sync.Once
   sync.Once可以在代码的任意位置初始化和调用,并且线程安全。sync.Once最大的作用就是延迟初始化,对于一个sync.Once变量我们并不会在程序启动的时候初始化,而是在第一次用到它的时候才会初始化,并且只初始化这一次,初始化之后驻留在内存里,这就非常适合我们之前提到的配置文件加载场景。设想一下,如果是在程序刚开始就加载配置,若迟迟未被使用,既浪费了内存,又延长了程序加载时间,而sync.Once就刚好解决了这个问题。
        使用示例: 

// 声明配置结构体Config
type Config struct{}var instance *Config
var once sync.Once     // 声明一个sync.Once变量// 获取配置结构体
func InitConfig() *Config {once.Do(func(){instance = &Config{}})return instance
}

        只有在第一次调用InitConfig()获取Config 指针的时候才会执行once.Do(func(){instance = &Config{} })语句,执行完之后instance就驻留在内存中,后面再次执行InitConfig()的时候,就直接返回内存中的instance。 

sync.Once与init()的区别

有时候我们使用init()方法进行初始化,init()方法是在其所在的package首次加载时执行的,而sync.Once可以在代码的任意位置初始化和调用,是在第一次用的它的时候才会初始化。

sync.Locker        控制并发安全的两种方式锁(mutex)和原子(atomic)

        说到并发编程,就不得不谈一个老生常谈的问题,那就是资源竞争,也就是我们这节要讲的并发安全。因为一旦开启了多个goroutine去处理问题,那么这些goroutine就有可能在同一时间操作同一个系统资源,比如同一个变量,同一份文件等等,这里我们如果不加控制的话,可能会出现并发安全问题

        在Go语言中,有两种方式来控制并发安全,锁(mutex)和原子(atomic)操作
        举个例子,看下面代码

package mainimport ("fmt""sync"
)var (num intwg  = sync.WaitGroup{}
)func add() {defer wg.Done()num += 1
}func main() {var n = 10 * 10 * 10 * 10wg.Add(n)for i := 0; i < n; i++ {// 启动n个goroutine去累加numgo add()}// 等待所有goroutine执行完毕wg.Wait()// 不出意外的话,num应该等于n,但是,但是,但是实际上不一致!fmt.Println(num == n)
}

运行结果:

false

        我们用n(这里是10000,可以自行修改,尽量数字大一点)个goroutine去给num做累加,最后并num并不等于n,这就是并发问题,同一时间有多个goroutine都在对num+1操作,但是后一个并不是在前一次执行完的基础之上运行的,可能两次运行num的初始相同,这样前一个num+1的结果就被后一个覆盖了,看起来好像只做了一个加法。

        为了避免类似的并发安全问题,我们一般会采用下面两种方式处理,在go语言中并发相关的都在sync包下面。 

锁  Mutex

锁Mutex不能复制使用
  1. 状态共享sync.Mutex内部维护了一个状态变量,用于记录锁的状态(如锁定、解锁、等待队列等)。如果复制Mutex,多个变量将共享同一个状态,导致锁的行为不可预测。

  2. 并发控制:Mutex的设计初衷是确保在并发环境中,只有一个goroutine能够访问共享资源。复制Mutex会破坏这一机制,使得多个goroutine可能同时持有锁,导致数据竞争。

  3. 内部实现sync.Mutex的内部实现使用了信号量和状态位等低级同步机制,这些机制不支持复制操作。

互斥锁Mutex

        互斥锁是一种最常用的控制并发安全的方式,它在同一时间只允许一个goroutine对共享资源进行访问。
        互斥锁的声明方式如下:

var lock sync.Mutex

互斥锁有两个方法

func (m *Mutex) Lock()     // 加锁
func (m *Mutex) Unlock()   // 解锁

        一个互斥锁只能同时被一个goroutine锁定,其它goroutine将阻塞直到互斥锁被解锁才能加锁成功。sync.Mutex在使用的时候要注意:对一个未锁定的互斥锁解锁将会产生运行时错误
        对上面的例子稍作修改,加上互斥锁: 

package mainimport ("fmt""sync"
)var (num intwg  = sync.WaitGroup{}// 我们用锁来保证num的并发安全mu = sync.Mutex{}
)func add() {mu.Lock()defer wg.Done()num += 1mu.Unlock()
}func main() {var n = 10 * 10 * 10 * 10wg.Add(n)for i := 0; i < n; i++ {// 启动n个goroutine去累加numgo add()}// 等待所有goroutine执行完毕wg.Wait()fmt.Println(num == n)
}

运行结果:

true

        我们可以自行修改n的值,在我们能开启足够多的goroutine的情况下,他结果一定会是true
        本例使用了前面介绍的sync.WaitGroup来等待所有协程执行结束。并且在add函数里使用了互斥锁来保证num += 1操作的并发安全,但是注意不要忘了用mu.Unlock来进行解锁,否则其他goroutine将一直等待加锁造成阻塞。 

 读写锁RWMutex

        读写锁就是将读操作和写操作分开,可以分别对读和写进行加锁,一般用在大量读操作、少量写操作的情况。
        读写锁的声明方式如下:

var rw sync.RWMutex

读写锁有两个方法:

func (rw *RWMutex) Lock()     // 对写锁加锁
func (rw *RWMutex) Unlock()   // 对写锁解锁func (rw *RWMutex) RLock()    // 对读锁加锁
func (rw *RWMutex) RUnlock()  // 对读锁解锁

读写锁的使用遵循以下几个法则:

  1. 同时只能有一个 goroutine 能够获得写锁定。
  2. 同时可以有任意多个 gorouinte 获得读锁定。
  3. 同时只能存在写锁定或读锁定(读和写互斥)。

        通俗理解就是可以多个goroutine同时读,但是只有一个goroutine能写,共享资源要么在被一个或多个goroutine读取,要么在被一个goroutine写入, 读写不能同时进行。
        读写锁示例: 

package mainimport ("fmt""sync""time"
)var cnt = 0func main() {var mr sync.RWMutexfor i := 1; i <= 3; i++ {go write(&mr, i)}for i := 1; i <= 3; i++ {go read(&mr, i)}time.Sleep(time.Second)fmt.Println("final count:", cnt)
}func read(mr *sync.RWMutex, i int) {fmt.Printf("goroutine%d reader start\n", i)mr.RLock()fmt.Printf("goroutine%d reading count:%d\n", i, cnt)time.Sleep(time.Millisecond)mr.RUnlock()fmt.Printf("goroutine%d reader over\n", i)
}func write(mr *sync.RWMutex, i int) {fmt.Printf("goroutine%d writer start\n", i)mr.Lock()cnt++fmt.Printf("goroutine%d writing count:%d\n", i, cnt)time.Sleep(time.Millisecond)mr.Unlock()fmt.Printf("goroutine%d writer over\n", i)
}

运行结果:

goroutine3 reader start
goroutine3 reading count:0
goroutine1 writer start
goroutine2 writer start
goroutine1 reader start
goroutine2 reader start
goroutine3 writer start
goroutine3 reader over
goroutine1 writing count:1
goroutine1 writer over
goroutine1 reading count:1
goroutine2 reading count:1
goroutine2 reader over
goroutine2 writing count:2
goroutine1 reader over
goroutine2 writer over    
goroutine3 writing count:3
goroutine3 writer over
final count: 3

        简单分析:首先goroutine3开始加了读锁,开始读取,读到count的值为0,然后goroutine1尝试写入,goroutine2尝试写入,但是都会阻塞,因为goroutine3加了读锁,不能再加写锁,在第8行goroutine3 读取完毕之后,goroutine1争抢到了锁,加了写锁,写完释放写锁之后,goroutine1goroutine2同时加了读锁,读到count的值为1。可以看到读写锁是互斥的,写写锁是互斥的,读读锁可以一起加。 

死锁

        提到锁,就有一个绕不开的话题:死锁。死锁就是一种状态,当两个或以上的goroutine在执行过程中,因争夺共享资源处在互相等待的状态,如果没有外部干涉将会一直处于这种阻塞状态,我们称这时的系统发生了死锁。死锁场景一般有以下两种:

1、Lock/Unlock不成对

        这类情况最常见的场景就是对锁进行拷贝使用

package main    import ("fmt""sync"
)func main() {var mu sync.Mutexmu.Lock()defer mu.Unlock()copyMutex(mu)
}func copyMutex(mu sync.Mutex) {mu.Lock()defer mu.Unlock()fmt.Println("ok")
}

运行结果:

fatal error: all goroutines are asleep - deadlock!      goroutine 1 [semacquire]:                               
sync.runtime_SemacquireMutex(0xc0000160ac, 0x0, 0x1)    D:/Program Files/Go/src/runtime/sema.go:71 +0x4e
sync.(*Mutex).lockSlow(0xc0000160a8)                    D:/Program Files/Go/src/sync/mutex.go:138 +0x10f
sync.(*Mutex).Lock(...)                                 D:/Program Files/Go/src/sync/mutex.go:81        
main.copyTest(0xc0000160a8)

        会报死锁,为什么呢?有的同学可能会注意到,这里mu sync.Mutex当作参数传入到函数copyMutex,锁进行了拷贝,不是原来的锁变量了,那么一把新的锁,在执行mu.Lock()的时候应该没问题。这就是要注意的地方,如果将带有锁结构的变量赋值给其他变量,锁的状态会复制。所以多锁复制后的新的锁拥有了原来的锁状态,那么在copyMutex函数内执行mu.Lock()的时候会一直阻塞,因为外层的main函数已经Lock()了一次,但是并没有机会Unlock(),导致内层函数会一直等待Lock(),而外层函数一直等待Unlock(),这样就造成了死锁所以在使用锁的时候,我们应当尽量避免锁拷贝,并且保证Lock()和Unlock()成对出现,没有成对出现容易会出现死锁的情况,或者是Unlock 一个未加锁的Mutex而导致 panic。

        尽量养成如下使用习惯 

mu.Lock()
defer mu.Unlock()
2、循环等待 

        另一个容易造成死锁的场景就是循环等待,A等B,B等C,C等A,循环等待

package mainimport ("sync""time"
)func main() {var mu1, mu2 sync.Mutexvar wg sync.WaitGroupwg.Add(2)go func() {defer wg.Done()mu1.Lock()defer mu1.Unlock()time.Sleep(1 * time.Second)mu2.Lock()defer mu2.Unlock()}()go func() {defer wg.Done()mu2.Lock()defer mu2.Unlock()time.Sleep(1 * time.Second)mu1.Lock()defer mu1.Unlock()}()wg.Wait()
}

运行结果:

fatal error: all goroutines are asleep - deadlock!

        死锁了,代码很简单,两个goroutine,一个goroutine先锁mu1,再锁mu2,另一个goroutine先锁mu2,再锁mu1,但是在它们进行第二次枷锁操作的时候,彼此等待对方释放锁,这样就造成了循环等待,一直阻塞,形成死锁。 

sync/atomic

        除了前面介绍的锁mutex以外,还有一种解决并发安全的策略,就是原子操作。所谓原子操作就是这一系列的操作在cpu上执行是一个不可分割的整体,显然要么全部执行,要么全部不执行,不会受到其他操作的影响,也就不会存在并发问题。

atomic和mutex的区别:
  1. 使用方式:通常mutex用于保护一段执行逻辑,而atomic主要是对变量进行操作
  2. 底层实现:mutex由操作系统调度器实现,而atomic操作有底层硬件指令支持,保证在cpu上执行不中断。所以atomic的性能也能随cpu的个数增加线性提升

atomic提供的方法 

func AddT(addr *T, delta T)(new T)
func StoreT(addr *T, val T)
func LoadT(addr *T) (val T)
func SwapT(addr *T, new T) (old T)
func CompareAndSwapT(addr *T, old, new T) (swapped bool)
T的类型是int32、int64、uint32、uint64和uintptr中的任意一种

这里就不一一演示各个方法了,以AddT方法为例简单看一个例子

package mainimport ("fmt""sync""sync/atomic"
)func main() {var sum int32 =  0var wg sync.WaitGroupfor i := 0; i < 100; i++ {wg.Add(1)go func() {defer wg.Done()atomic.AddInt32(&sum, 1)}()}wg.Wait()fmt.Printf("sum is %d\n",sum)
}

100个goroutine,每个goroutine都对sum+1,最后结果为100。 

atomic.Value

       上面展示的AddTStoreT等方法都是针对的基本数据类型做的操作,假设想对多个变量进行同步保护,即假设想对一个struct这样的复合类型用原子操作,也是支持的吗?也可以做支持,go语言里的atomic.value支持任意一种接口类型进行原子操作,且提供了LoadStoreSwapCompareAndSwap四种方法:

  • Load:func (v *Value) Load() (val any),从value读出数据
  • Store:func (v *Value) Store(val any),向value写入数据
  • Swap:func (v *Value) Swap(new any) (old any),用new交换value中存储的数据,返回value原来存储的旧数据
  • CompareAndSwap:func (v *Value) CompareAndSwap(old, new any) (swapped bool),比较value中存储的数据和old是否相同,相同的话,将value中的数据替换为new

代码示例

package mainimport ("fmt""sync/atomic"
)type Student struct {Name stringAge  int
}func main() {st1 := Student{Name: "zhangsan",Age:  18,}st2 := Student{Name: "lisi",Age:  19,}st3 := Student{Name: "wangwu",Age:  20,}var v atomic.Valuev.Store(st1)fmt.Println(v.Load().(Student))old := v.Swap(st2)fmt.Printf("after swap: v=%v\n", v.Load().(Student))fmt.Printf("after swap: old=%v\n", old)swapped := v.CompareAndSwap(st1, st3)   // v中存储的和st1不相同,交换失败fmt.Println("compare st1 and v\n", swapped, v)swapped = v.CompareAndSwap(st2, st3)   // v中存储的和st2相同,交换成功,v中变为st3fmt.Println("compare st2 and v\n", swapped, v)
}

运行结果 :

{zhangsan 18}
after swap: v={lisi 19}
after swap: old={zhangsan 18}
compare st1 and vfalse {{lisi 19}}
compare st2 and vtrue {{wangwu 20}}

sync.Map

        Go语言内置的Map不是并发安全的,在多个goroutine同时操作map的时候,会有并发问题
具体看下面例子

package mainimport ("fmt""strconv""sync"
)var m = make(map[string]int)func getVal(key string) int {return m[key]
}func setVal(key string, value int) {m[key] = value
}func main() {wg := sync.WaitGroup{}wg.Add(10)for i := 0; i < 10; i++ {go func(num int) {defer wg.Done()key := strconv.Itoa(num)setVal(key, num)fmt.Printf("key=:%v,val:=%v\n", key, getVal(key))}(i)}wg.Wait()
}

运行结果:

fatal error: concurrent map writes

        程序报错了,说明map不能同时被多个goroutine读写。要解决map的并发写问题一种方式使用我们前面学到的对map加锁,这样就可以了 

package mainimport ("fmt""strconv""sync"
)var m = make(map[string]int)
var mu sync.Mutexfunc getVal(key string) int {return m[key]
}func setVal(key string, value int) {m[key] = value
}func main() {wg := sync.WaitGroup{}wg.Add(10)for i := 0; i < 10; i++ {go func(num int) {defer func() {wg.Done()mu.Unlock()}()key := strconv.Itoa(num)mu.Lock()setVal(key, num)fmt.Printf("key=:%v,val:=%v\n", key, getVal(key))}(i)}wg.Wait()
}

运行结果:

key=:9,val:=9
key=:4,val:=4
key=:0,val:=0
key=:1,val:=1
key=:2,val:=2
key=:3,val:=3
key=:6,val:=6
key=:7,val:=7
key=:5,val:=5
key=:8,val:=8

        另外一种方式是使用sync包中提供的一个开箱即用的并发安全版mapsync.Map,在 Go 1.9 引入。sync.Map不用初始化就可以使用,同时sync.Map内置了诸如StoreLoadLoadOrStoreDeleteRange等操作方法。
具体使用方法看示例: 

package mainimport ("fmt""sync"
)func main() {var m sync.Map// 1. 写入m.Store("name", "zhangsan")m.Store("age", 18)// 2. 读取age, _ := m.Load("age")fmt.Println(age.(int))// 3. 遍历m.Range(func(key, value interface{}) bool {fmt.Printf("key is:%v, val is:%v\n", key, value)return true})// 4. 删除m.Delete("age")age, ok := m.Load("age")fmt.Println(age, ok)// 5. 读取或写入m.LoadOrStore("name", "zhangsan")name, _ := m.Load("name")fmt.Println(name)
}

运行结果: 

18
key is:name, val is:zhangsan
key is:age, val is:18       
<nil> false                 
zhangsan
  1. 通过store方法写入两个键值对
  2. 读取key为age的值,读出来age为18
  3. 通过range方法遍历map的key和value
  4. 删除key为age的键值对,删除完之后,再次读取age,age为空,ok为false表示map里没有这个key
  5. LoadOrStore尝试读取key为name的值,读取不到就写入键值对name-zhangsan,能读取到就返回原来map里的name对应的值

注意:sync.Map 没有提供获取 map 数量的方法,需要我们在对 sync.Map进行遍历时自行计算,sync.Map 为了保证并发安全有一些性能损失,因此在非并发情况下,使用 map 相比使用 sync.Map 会有更好的性能 

sync.Pool

  sync.Pool是在sync包下的一个内存池组件,用来实现对象的复用,避免重复创建相同的对象,造成频繁的内存分配和gc,以达到提升程序性能的目的。虽然池子中的对象可以被复用,但是是sync.Pool并不会永久保存这个对象,池子中的对象会在一定时间后被gc回收,这个时间是随机的。所以,sync.Pool来持久化存储对象是不可取的。
        另外,sync.Pool本身是并发安全的,支持多个goroutine并发的往sync.Pool存取数据

sync.Pool使用方法

关于sync.Pool的使用,一般是通过三个方法来完成的

方法说明
New()sync.Pool的构造函数,用于指定sync.Pool中缓存的数据类型,当调用Get方法从对象池中获取对象的时候,对象池中如果没有,会调用New方法创建一个新的对象
Get()从对象池取对象
Put()往对象池放对象,下次Get的时候可以复用

下面通过例子看一下sync.Pool的使用方式

package mainimport ("fmt""sync"
)type Student struct {Name stringAge  int
}func main() {pool := sync.Pool{New: func() interface{} {return &Student{Name: "zhangsan",Age:  18,}},}st := pool.Get().(*Student)println(st.Name, st.Age)fmt.Printf("addr is %p\n", st)pool.Put(st)st1 := pool.Get().(*Student)println(st1.Name, st1.Age)fmt.Printf("addr1 is %p\n", st1)
}

程序输出

zhangsan 18
addr is 0x140000a0018
zhangsan 18
addr1 is 0x140000a0018

        在程序中,首先初始化一个sync.Pool对象,初始化里面的New方法,用于创建对象,这里是返回一个Student类型的指针。第一次调用pool.Get().(*Student)的时候,由于池子内没有对象,所以会通过New方法创建一个,注意pool.Get()返回的是一个interface{},所以我们需要断言成*Student类型,在我们使用完,打印出NameAge之后,再调用Put方法,将这个对象放回到池子内,后面我们紧接着又调用pool.Get()取对象,可以看到两次去取的对象地址是同一个,说明是同一个对象,表明sync.Pool有缓存对象的功能。

注意:
我们在第一次pool.Get()取出*Student对象打印完地址之后,put进池子的时候没有进行一个Reset的过程,这里是因为我们取出*Student对象之后,仅仅是读取里面的字段,并没有修改操作,假设我们有修改操作,那么这里就需要在pool.Put(st)之前执行Reset,将对象的值复原,如果不这样做,那么下一次pool.Get()取出的*Student对象就不是我们希望复用的初始对象

假设我们对*Student做修改

package mainimport ("fmt""sync"
)type Student struct {Name stringAge  int
}func main() {pool := sync.Pool{New: func() interface{} {return &Student{Name: "zhangsan",Age:  18,}},}st := pool.Get().(*Student)println(st.Name, st.Age)fmt.Printf("addr is %p\n", st)// 修改st.Name = "lisi"st.Age = 20// 回收pool.Put(st)st1 := pool.Get().(*Student)println(st1.Name, st1.Age)fmt.Printf("addr1 is %p\n", st1)
}

程序输出

zhangsan 18
addr is 0x1400000c030
lisi 20
addr1 is 0x1400000c030

        可以看到,我们第二次取出的对象虽然和第一次是同一个,地址形同,但是对象的字段值却发生了变化,不是我们初始化的对象了,我们想要一直重复使用一个相同的对象的话,显然这里有问题。所以,我们需要在pool.Put(st)回收对象之前,进行对象的Reset操作,将对象值复原,同时在每次我们pool.Get()取出完对象使用完毕之后,也不要忘了调用pool.Put方法把对象再次放入对象池,以便对象能够复用。 

sync.Pool的使用场景

  1. sync.pool主要是通过对象复用来降低gc带来的性能损耗,所以在高并发场景下,由于每个goroutine都可能过于频繁的创建一些大对象,造成gc压力很大。所以在高并发业务场景下出现 GC 问题时,可以使用 sync.Pool 减少 GC 负担
  2. sync.pool不适合存储带状态的对象,比如socket 连接、数据库连接等,因为里面的对象随时可能会被gc回收释放掉
  3. 不适合需要控制缓存对象个数的场景,因为Pool 池里面的对象个数是随机变化的,因为池子里的对象是会被gc的,且释放时机是随机的 
http://www.dtcms.com/a/314260.html

相关文章:

  • 深度解析:CPU 与 GPU 上的张量运算,为何“快”与“慢”并非绝对?
  • 亚马逊撤离Google购物广告:重构流量生态的战略博弈
  • 从零开始搞定类与对象(中)
  • 企业架构被大模型重构:大模型驱动下的数字基建革命与机遇
  • 操作系统:RPC 中可能遇到的问题(Issues in RPC)
  • Ubuntu系统VScode实现opencv(c++)图像一维直方图
  • Git如何同步本地与远程仓库并解决冲突
  • C#利用unity游戏引实现开发设备仿真系统步骤
  • 《解构Angular组件变化检测:从自动到手 动的效能突破》
  • Unity Shader编程完全入门指南:从零到实战 C# 实战案例
  • 雷达系统工程学习:自制极化合成孔径雷达无人机
  • 【OpenGL】LearnOpenGL学习笔记03 - 着色器
  • 2025年半导体探针卡市场深度调研:规模数据、竞争格局
  • 防火墙的进阶练习
  • PVE环境对网口和wifi的配置
  • Neo4j 基础语法指南
  • 基于Spring Cloud Gateway和Resilience4j的微服务容错与流量控制实战经验分享
  • javacc学习笔记 03、编译原理实践 - JavaCC解析表达式并生成抽象语法树
  • MySQL5.0数据库管理系统安装部署
  • PCB反焊盘的样子越诡异,高速过孔的性能越好?
  • [自动化Adapt] 父子事件| 冗余过滤 | SQLite | SQLAlchemy | 会话工厂 | Alembic
  • 【物联网】基于树莓派的物联网开发【23】——树莓派安装SQLite嵌入式数据库
  • 秋招笔记-8.4
  • 小实验:按键点灯(中断法)
  • QT的UDP
  • 【数据结构入门】链表
  • Solidity智能合约开发全攻略
  • Java基础-斗地主游戏
  • ArrayDeque双端队列--底层原理可视化
  • ubuntu修改时区