【Go-补充】Sync包

并发编程-Sync包

sync.WaitGroup

在代码中生硬的使用time.Sleep肯定是不合适的,Go语言中可以使用sync.WaitGroup来实现并发任务的同步。 sync.WaitGroup有以下几个方法:

方法名 功能
(wg * WaitGroup) Add(delta int) 计数器+delta
(wg *WaitGroup) Done() 计数器-1
(wg *WaitGroup) Wait() 阻塞直到计数器变为0

sync.WaitGroup内部维护着一个计数器,计数器的值可以增加和减少。例如当我们启动了N 个并发任务时,就将计数器值增加N。每个任务完成时通过调用Done()方法将计数器减1。通过调用Wait()来等待并发任务执行完,当计数器值为0时,表示所有并发任务已经完成。

我们利用sync.WaitGroup将上面的代码优化一下:

go 复制代码
var wg sync.WaitGroup

func hello() {
    defer wg.Done()
    fmt.Println("Hello Goroutine!")
}
func main() {
    wg.Add(1)
    go hello() // 启动另外一个goroutine去执行hello函数
    fmt.Println("main goroutine done!")
    wg.Wait()
}

需要注意sync.WaitGroup是一个结构体,传递的时候要传递指针。

sync.Once

sync.Once 是 Go 语言 sync 包提供的一个同步原语,用于确保某个操作只执行一次,无论它被调用多少次或有多少个 Goroutine 同时尝试执行它。

它的主要用途包括:

1.延迟初始化 (Lazy Initialization) :当你想在资源第一次被需要时才进行初始化,而不是在程序启动时就初始化,sync.Once 可以确保初始化只发生一次,从而避免重复初始化的开销。

go 复制代码
import (
	"fmt"
	"sync"
	"time"
)

var once sync.Once
var expensiveResource string

func getExpensiveResource() string {
	once.Do(func() {
		fmt.Println("正在初始化昂贵资源......")
		time.Sleep(100 * time.Millisecond) // 模拟初始化耗时
		expensiveResource = "这是一个昂贵的资源"
	})
	return expensiveResource
}

func main() {
	var wg sync.WaitGroup
	for i := 0; i < 5; i++ {
		wg.Add(1)
		go func() {
			defer wg.Done()
			fmt.Println(getExpensiveResource())
		}()
	}
	wg.Wait()
	fmt.Println("所有 Goroutine 已完成。")
}

在上面的例子中,"正在初始化昂贵资源..." 只会被打印一次,即使 getExpensiveResource 被多个 Goroutine 调用。

2.单例模式 (Singleton Pattern):确保一个类的实例只被创建一次。

go 复制代码
package main

import (
	"fmt"
	"sync"
)

type Singleton struct {
	Name string
}

var (
	instance *Singleton
	once     sync.Once
)

func GetSingletonInstance() *Singleton {
	once.Do(func() {
		fmt.Println("create Singleton instance")
		instance = &Singleton{Name: "my Singleton instance"}
	})
	return instance
}

func main() {
	var wg sync.WaitGroup
	for i := 0; i < 5; i++ {
		wg.Add(1)
		go func() {
			defer wg.Done()
			s := GetSingletonInstance()
			fmt.Println(s.Name)
		}()
	}
	wg.Wait()
}

3.包级别初始化 (Package-Level Initialization):在包加载时进行一次性设置。

sync.Once 的新特性 (Go 1.21 及更高版本)

Go 1.21 引入了 OnceFunc, OnceValue, 和 OnceValues,它们是 sync.Once 的封装,提供了更简洁的用法:

  • OnceFunc(f func()) func() : 返回一个函数,该函数只会调用 f 一次。多次调用返回的函数都会执行,但 f 只会在第一次被调用。
  • OnceValue[T any](f func() T) func() T : 返回一个函数,该函数只会调用 f 一次并缓存 f 的返回值。后续调用都会返回相同的值。
  • OnceValues[T1, T2 any](f func() (T1, T2)) func() (T1, T2) : 类似于 OnceValue,但用于返回两个值。

示例:OnceValue

go 复制代码
package main

import (
	"fmt"
	"sync"
)

func main() {
	// 定义一个只计算一次并返回结果的函数
	getSumOnce := sync.OnceValue(func() int {
		sum := 0
		for i := 0; i < 1000; i++ {
			sum += i
		}
		fmt.Println("计算了一次:", sum)
		return sum
	})

	var wg sync.WaitGroup
	for i := 0; i < 10; i++ {
		wg.Add(1)
		go func() {
			defer wg.Done()
			result := getSumOnce() // 这里开辟10个协程 执行函数,但实际只执行了一次
			fmt.Printf("Goroutine %d 得到结果: %d\n", i, result)
		}()
	}
	wg.Wait()
}

在这个例子中,getSumOnce 函数只会在第一次被调用时执行内部的求和逻辑,并打印 "计算了一次:",之后的调用会直接返回缓存的结果。

重要注意事项:

  • sync.Once 实例一旦执行过其 Do 方法,就不能被重置。
  • 如果传递给 Do 方法的函数发生 panic,sync.Once 仍会认为该操作已完成,后续调用不会再次执行该函数。
  • sync.Once 不应被复制。

总而言之,sync.Once 是 Go 中一个强大且常用的同步工具,用于安全地执行一次性初始化。

sync.Map

sync.Map 是 Go 语言 sync 包提供的一个并发安全的哈希映射(map),它旨在解决 Go 内置 map 在并发访问时需要外部 sync.RWMutex 保护的性能问题,尤其是在读多写少的场景下。

Go 语言内置的 map 不是并发安全的。如果在多个 Goroutine 中同时对一个 map 进行读写操作,会导致数据竞争(data race),进而引发程序崩溃或产生不确定的行为。为了解决这个问题,通常需要使用 sync.RWMutex 来保护 map 的并发访问

go 复制代码
var m = make(map[string]int)

func get(key string) int {
	return m[key]
}

func set(key string, value int) {
	m[key] = value
}

func main() {
	wg := sync.WaitGroup{}
	for i := 0; i < 20; i++ {
		wg.Add(1)
		go func(n int) {
			key := strconv.Itoa(n)
			set(key, n)
			fmt.Printf("k=:%v,v:=%v\n", key, get(key))
			wg.Done()
		}(i)
	}
	wg.Wait()
}

上面的代码开启少量几个goroutine的时候可能没什么问题,当并发多了之后执行上面的代码就会报fatal error: concurrent map writes错误。

像这种场景下就需要为map加锁来保证并发的安全性了,Go语言的sync包中提供了一个开箱即用的并发安全版map--sync.Map。开箱即用表示不用像内置的map一样使用make函数初始化就能直接使用。同时sync.Map内置了诸如Store、Load、LoadOrStore、Delete、Range等操作方法

sync.Map 的常用方法:

  • Store(key, value interface{}): 将 key-value 对存储到 Map 中。

  • Load(key interface{}) (value interface{}, ok bool): 根据 key 加载值。如果 key 存在,返回 value 和 true;否则返回 nil 和 false。

  • LoadOrStore(key, value interface{}) (actual interface{}, loaded bool): 如果 key 存在,则返回其现有值和 true;否则,存储新的 key-value 对并返回 value 和 false。这是一个非常常用的方法,可以实现"如果不存在就存储,否则就加载"的逻辑。

  • Delete(key interface{}): 从 Map 中删除 key 及其对应的值。

  • Range(f func(key, value interface{}) bool): 遍历 Map 中的所有 key-value 对,对每个对调用提供的函数 f。如果 f 返回 false,则停止遍历。注意:在 Range 期间,Map 可能会被并发修改,所以遍历的结果可能不是一个完全一致的快照。

go 复制代码
var m = sync.Map{}

func main() {
	wg := sync.WaitGroup{}
	for i := 0; i < 10; i++ {
		wg.Add(1)
		go func(n int) {
			key := strconv.Itoa(n)
			m.Store(key, n)
			value, _ := m.Load(key)
			fmt.Printf("k=:%v,v:=%v\n", key, value)
			wg.Done()
		}(i)
	}
	wg.Wait()
}
相关推荐
why1517 分钟前
字节golang后端二面
开发语言·后端·golang
RedJACK~7 分钟前
【Go语言】Ebiten游戏库开发者文档 (v2.8.8)
开发语言·游戏·golang
lyh13449 分钟前
【Go语言生态】
开发语言
还是鼠鼠9 分钟前
单元测试-断言&常见注解
java·开发语言·后端·单元测试·maven
我们的五年12 分钟前
【Qt】Bug:findChildren找不到控件
开发语言·qt·bug
cainiao08060518 分钟前
Spring Boot 4.0实战:构建高并发电商系统
java·spring boot·后端
虾球xz24 分钟前
CppCon 2014 学习:Decomposing a Problem for Parallel Execution
开发语言·c++·学习
Chandler241 小时前
Go 即时通讯系统:日志模块重构,并从main函数开始
后端·重构·golang·gin
小智学长 | 嵌入式1 小时前
SOC-ESP32S3部分:25-HTTP请求
开发语言·单片机·esp32
Biomamba生信基地1 小时前
R语言基础| 创建数据集
开发语言·r语言·生信·医药