概述
谈到并发编程肯定就绕不开对共享资源的访问控制,也就是我们平时开发中所使用的一些同步工具,比如同步锁、并发安全的数据结构等。Go中的同步工具都放在sync这个工具包下面,今天就针对这些常用的同步工具一一介绍。
并发前导内容
在学习Go的同步工具之前,我们先回忆一下在并发情况的同步访问控制的一些基础概念,所有同步的机制都是基于这些基本概念去设计。
竞态条件
一旦某个数据被多个线程所共享并操作,那么就会产生竞争和冲突,这种条件称为竞态条件,也就是多个线程同时操作会产生冲突,这种竞争的场景我们称为竞态条件,而这个条件背后的资源就被称为共享资源,所以我们经常说的共享资源的一致性控制其实就是处理这种竞态条件。
临界区
线程通过操作某块代码从而操作后面的共享资源,如果必须保证所有线程都必须同步访问这块儿代码,那么这块儿代码区域即为临界区。只有访问临界区才能操作共享资源。
同步工具
上面说的竞态条件和临界区就是要保证同一时间只有一个线程在操作,也就是说针对临界区的访问必须是串行的,而控制这个流程的就是同步工具了,同步工具就是分发一个令牌,持有令牌的才能进入临界区,操作完之后归还临牌。
Sync包
Sync是Go里面的一个同步工具包,通过这些工具实现Go的同步访问机制。
Mutex
互斥锁,和Java里面的synchronized作用类似,只不过底层实现的机制不一样,Mutex是一种悲观锁,同时也是一种可重入锁,操作共享资源之前必须先加锁。
func TestCounterThreadSafe(t *testing.T) {
var mut sync.Mutex
counter := 0
for i := 0; i < 5000; i++ {
go func() {
defer func() {
mut.Unlock()
}()
mut.Lock()
counter++
}()
}
time.Sleep(1 * time.Second)
t.Logf("counter = %d", counter) //输出5000
}
RWMutex
RWMutex读写锁是Mutex的一个扩展,和Java中的ReentrantReadWriteLock功能类似,读写锁本质上也是互斥锁也就是悲观锁,只不过分了读锁和写锁写锁两种场景。
var (
data = make(map[string]string)
mutex sync.RWMutex
)
func readData(key string) {
mutex.RLock()
defer mutex.RUnlock()
fmt.Println("Reading data with key:", key, " - Value:", data[key])
time.Sleep(time.Millisecond) // 模拟读操作耗时
}
func writeData(key, value string) {
mutex.Lock()
defer mutex.Unlock()
data[key] = value
fmt.Println("Writing data with key:", key, " - Value:", value)
time.Sleep(time.Millisecond) // 模拟写操作耗时
}
func TestRWMutex(t *testing.T) {
for i := 0; i < 3; i++ {
go readData("key") // 启动多个goroutine进行读操作
}
for i := 0; i < 2; i++ {
go writeData("key", fmt.Sprintf("value%d", i)) // 启动两个goroutine进行写操作
}
time.Sleep(time.Second) // 等待goroutine执行完成
}
只有读锁和读锁是可以共享,其他场景都是互斥的。
Cond
条件变量,用于在多个goroutine之间建立条件等待和广播通知的机制,类似于Java中的wait和notify机制。
Go里面的Cond必须配合Mutex使用,用来在多个goroutine进行条件判断和传递,回想一下我们在Java中使用wait和notify是不是也要获取到对应的对象锁才可以进行这两个操作,道理是一样的。
package main
import (
"log"
"sync"
"time"
)
func main() {
// mailbox 代表信箱。
// 0代表信箱是空的,1代表信箱是满的。
var mailbox uint8
// lock 代表信箱上的锁。
var lock sync.Mutex
// sendCond 代表专用于发信的条件变量。
sendCond := sync.NewCond(&lock)
// recvCond 代表专用于收信的条件变量。
recvCond := sync.NewCond(&lock)
// send 代表用于发信的函数。
send := func(id, index int) {
lock.Lock()
for mailbox == 1 {
sendCond.Wait()
}
log.Printf("sender [%d-%d]: the mailbox is empty.",
id, index)
mailbox = 1
log.Printf("sender [%d-%d]: the letter has been sent.",
id, index)
lock.Unlock()
recvCond.Broadcast()
}
// recv 代表用于收信的函数。
recv := func(id, index int) {
lock.Lock()
for mailbox == 0 {
recvCond.Wait()
}
log.Printf("receiver [%d-%d]: the mailbox is full.",
id, index)
mailbox = 0
log.Printf("receiver [%d-%d]: the letter has been received.",
id, index)
lock.Unlock()
sendCond.Signal() // 确定只会有一个发信的goroutine。
}
// sign 用于传递演示完成的信号。
sign := make(chan struct{}, 3)
max := 6
go func(id, max int) { // 用于发信。
defer func() {
sign <- struct{}{}
}()
for i := 1; i <= max; i++ {
time.Sleep(time.Millisecond * 500)
send(id, i)
}
}(0, max)
go func(id, max int) { // 用于收信。
defer func() {
sign <- struct{}{}
}()
for j := 1; j <= max; j++ {
time.Sleep(time.Millisecond * 200)
recv(id, j)
}
}(1, max/2)
go func(id, max int) { // 用于收信。
defer func() {
sign <- struct{}{}
}()
for k := 1; k <= max; k++ {
time.Sleep(time.Millisecond * 200)
recv(id, k)
}
}(2, max/2)
<-sign
<-sign
<-sign
}
WaitGroup
等待组,用于等待一组goroutine的结束。它可以用于等待一组goroutine全部完成后再继续执行后续的代码。和Java里面的CountDownLatch功能类似。
func TestWaitGroup(t *testing.T) {
var wg sync.WaitGroup
//添加信号量
wg.Add(2)
num := int32(0)
fmt.Printf("The number: %d [with sync.WaitGroup]\n", num)
for i := 0; i < 2; i++ {
go func() {
num++
//消耗信号量
wg.Done()
}()
}
//主程序等待信号量为0
wg.Wait()
println(num)
}
Once
只执行一次,即被Once同步的代码在并发情况下只会执行一次,大家第一个想到的场景是不是就是单例创建的场景。
type Singleton struct {
Data string
}
var instance *Singleton
var once sync.Once
func getInstance() *Singleton {
once.Do(func() {
instance = &Singleton{Data: "Singleton Instance Initialized"}
})
return instance
}
func TestOnce(t *testing.T) {
var wg sync.WaitGroup
//添加信号量
wg.Add(3)
// 获取单例实例
for i := 0; i < 3; i++ {
go func() {
singleton := getInstance()
fmt.Printf("Singleton Data: %p\n", singleton)
wg.Done()
}()
}
wg.Wait()
}
//输出的地址值完全一样,说明在并发条件下只创建了一个对象
Singleton Data: 0x14000054650
Singleton Data: 0x14000054650
Singleton Data: 0x14000054650
Atomic
Atomic提供了一组用于执行低级原子操作的函数。这些原子操作可以确保在并发环境下,操作的执行是原子的,不会被其他goroutine中断,从而避免了竞态条件的发生,和Java里面的原子性操作类似。
原子性自增:
func TestCounterThreadSafe(t *testing.T) {
var num int32
for i := 0; i < 5000; i++ {
go func() {
atomic.AddInt32(&num, 1)
}()
}
time.Sleep(1 * time.Second)
t.Logf("counter = %d", num) //输出5000
}
CAS操作:
func TestCASSafe(t *testing.T) {
var num int32 = 42
// 如果num的值等于old值,则用new值替换num的值,并返回true;否则不做替换,返回false
oldValue := int32(42)
newValue := int32(100)
swapped := atomic.CompareAndSwapInt32(&num, oldValue, newValue)
fmt.Println("Swapped:", swapped) //true
fmt.Println("New Value:", num) //100
}
基于CAS实现一个自旋锁:
type SpinLock struct {
flag int32
}
func (sl *SpinLock) Lock() {
for !atomic.CompareAndSwapInt32(&sl.flag, 0, 1) {
// 自旋等待,直到成功获取锁
fmt.Println("获取到锁")
break
}
}
func (sl *SpinLock) Unlock() {
atomic.StoreInt32(&sl.flag, 0) // 释放锁
}
func TestCAS(t *testing.T) {
var spinLock SpinLock
var wg sync.WaitGroup
for i := 0; i < 5; i++ {
wg.Add(1)
go func(id int) {
defer wg.Done()
spinLock.Lock()
defer spinLock.Unlock()
fmt.Println("Goroutine", id, "acquired the lock")
}(i)
}
wg.Wait()
}
Map
并发安全的Map,是 Go 1.9 引入的一种并发安全的映射类型。它提供了一种在多个 goroutine 之间安全地存储和读取键值对的方式,而无需使用额外的锁来保护数据。
// IntStrMap 代表键类型为int、值类型为string的并发安全字典。
type IntStrMap struct {
m sync.Map
}
func (iMap *IntStrMap) Delete(key int) {
iMap.m.Delete(key)
}
func (iMap *IntStrMap) Load(key int) (value string, ok bool) {
v, ok := iMap.m.Load(key)
if v != nil {
value = v.(string)
}
return
}
func (iMap *IntStrMap) LoadOrStore(key int, value string) (actual string, loaded bool) {
a, loaded := iMap.m.LoadOrStore(key, value)
actual = a.(string)
return
}
func (iMap *IntStrMap) Range(f func(key int, value string) bool) {
f1 := func(key, value interface{}) bool {
return f(key.(int), value.(string))
}
iMap.m.Range(f1)
}
func (iMap *IntStrMap) Store(key int, value string) {
iMap.m.Store(key, value)
}
Pool
sync.Pool
是 Go 语言标准库中的一个对象池,用于存储和复用临时对象,从而降低对象的分配和垃圾回收的压力。对象池可以提高程序的性能,特别是在需要频繁分配和释放临时对象的场景下。
sync.Pool
的主要特点如下:
-
自动复用:
sync.Pool
内部维护了一个对象的集合。当需要一个新对象时,它首先尝试从池中获取一个已经存在的对象,如果池中没有可用对象,则会调用用户提供的New
函数创建一个新对象。 -
生命周期管理: 对象池中的对象并不会被池所拥有,当对象不再被引用时,会被垃圾回收。对象池不保证对象的存活时间,可能在任何时候被清除。
-
并发安全:
sync.Pool
的所有方法都是并发安全的,可以安全地在多个 goroutine 中使用。
package main
import (
"fmt"
"sync"
)
func main() {
pool := &sync.Pool{
New: func() interface{} {
return "New Object"
},
}
// 从对象池获取对象
obj1 := pool.Get().(string)
fmt.Println("Object 1:", obj1)
// 将对象放回对象池
pool.Put("Reused Object")
// 再次从对象池获取对象
obj2 := pool.Get().(string)
fmt.Println("Object 2:", obj2)
}
关于Go的同步工具就介绍到这里,上面只是列举了一些同步工具的使用,在实际项目开发过程中需要根据不同的场景选择不同的同步工具。