【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()
}
相关推荐
序安InToo9 分钟前
第6课|注释与代码风格
后端·操作系统·嵌入式
xyy1239 分钟前
C#: Newtonsoft.Json 到 System.Text.Json 迁移避坑指南
后端
洋洋技术笔记12 分钟前
Spring Boot Web MVC配置详解
spring boot·后端
JxWang0512 分钟前
VS Code 配置 Markdown 环境
后端
navms16 分钟前
搞懂线程池,先把 Worker 机制啃明白
后端
JxWang0516 分钟前
离线数仓的优化及重构
后端
Nyarlathotep011317 分钟前
gin01:初探gin的启动
后端·go
JxWang0517 分钟前
安卓手机配置通用多屏协同及自动化脚本
后端
JxWang0518 分钟前
Windows Terminal 配置 oh-my-posh
后端
SimonKing34 分钟前
OpenCode AI编程助手如何添加Skills,优化项目!
java·后端·程序员