目录
1 Go并发
1.1 WaitGroup
sync.WaitGroup
是 Go 标准库提供的一种同步原语
,常用于等待一组 Goroutine
执行完成。它提供了一种简单的方式来管理多个 Goroutine
的并发执行,确保主程序在所有 Goroutine
完成后再继续。
在多线程(Goroutine
)编程中,主线程(main 函数
)需要等待子 Goroutine
完成工作后才能退出。如果直接退出,子 Goroutine
的执行会被中断。这时就需要 WaitGroup
来协调这些 Goroutine
的执行。
注意
:
- 避免重复使用
WaitGroup
:
一个WaitGroup
实例不应该在计数器值已经变为 0 后再次调用 Add() 或 Done()。 WaitGroup
是一个值类型
必须传递指针给Goroutine
,否则每个Goroutine
会得到一个拷贝,无法正确修改计数器。- 计数器不能为负数:
如果Done()
调用次数多于Add()
,会引发运行时错误。
WaitGroup
的核心方法有以下几个:
Add(delta int)
增加等待计数器的值,delta
是变化值。
每次启动一个Goroutine
时,调用 Add(1) 来增加计数。
可以使用负数来减少计数,但通常减少计数用Done
。Done()
将等待计数器减一,表示某个Goroutine
完成了工作。
每个Goroutine
执行完毕后,都应该调用Done()
。Wait()
阻塞主线程,直到等待计数器变为 0。
主线程调用Wait()
,表示等待所有计数器的Goroutine
执行完毕。
同步多个 Goroutine:
package main
import (
"fmt"
"sync"
)
func worker(id int, wg *sync.WaitGroup) {
defer wg.Done() // Goroutine 完成时调用 Done()
fmt.Printf("Worker %d started\n", id)
fmt.Printf("Worker %d finished\n", id)
}
func main() {
var wg sync.WaitGroup
for i := 1; i <= 3; i++ {
wg.Add(1) // 增加计数器
go worker(i, &wg)
}
wg.Wait() // 等待所有 Goroutine 完成
fmt.Println("All workers done")
}
结果:
Worker 1 started
Worker 1 finished
Worker 2 started
Worker 2 finished
Worker 3 started
Worker 3 finished
All workers done
1.2 并发锁
在go 代码中可能会存着多个 goroutine 同时操作一个资源(临界区),这种情况会发生竞态。
1.2.1 互斥锁
互斥锁
是一种常用的控制共享资源访问的方法,它能够保证只有一个 goroutine
访问共享资源。例如:网上购票。
互斥锁作用:同一时间有且仅有一个 goroutine
进入临界区,其他 goroutine
则在等待锁,等互斥锁释放后,等待的 goroutine
才可以获取锁进入临界区,多个 goroutine
都在等待一个锁时,唤醒机制是随机的。
示例:资源竞争的情况
package main
import (
"fmt"
"sync"
)
// 全局变量
var x int64
// 计时器
var sw sync.WaitGroup
// 累加函数
func add() {
defer sw.Done()
for i:=0;i<5000;i++ {
x++ // 不同 goroutine 竞争 x 资源
}
}
func main() {
sw.Add(2)
go add()
go add()
sw.Wait()
fmt.Println(x)
}
/*
6115/10000/7436......
会有各种情况
*/
示例:加互斥锁
package main
import (
"fmt"
"sync"
)
// 全局变量
var x int64
// 计时器
var sw sync.WaitGroup
// 互斥锁
var lock sync.Mutex
// 累加函数
func add() {
for i:=0;i<5000;i++ {
lock.Lock() // 互斥锁把门锁上
x++
lock.Unlock() // 互斥锁把门解锁
}
sw.Done()
}
func main() {
sw.Add(2)
go add()
go add()
sw.Wait()
fmt.Println(x)
}
/*
10000
*/
使用信道 解决竞争
package main
import (
"fmt"
"sync"
)
var x = 0
func increment(wg *sync.WaitGroup, ch chan bool) {
ch <- true
x = x + 1
<- ch
wg.Done()
}
func main() {
var w sync.WaitGroup
ch := make(chan bool, 1)
for i := 0; i < 1000; i++ {
w.Add(1)
go increment(&w, ch)
}
w.Wait()
fmt.Println("final value of x", x)
}
在上述程序中,我们创建了容量为 1 的缓冲信道,并将它传入 increment 协程。该缓冲信道用于保证只有一个协程访问增加 x 的临界区。原理是:方法是在 x 增加之前,传入 true 给缓冲信道。由于缓冲信道的容量为 1,其他协程试图往通道写数据时被阻塞,当 x 增加后,信道的值被读取。实际上这就保证了只允许一个协程访问临界区。
1.2.2 读写互斥锁
互斥锁是完全互斥的,但是很多的实际场景,读多写少,当并发的去读取一个资源不涉及资源修改的时候是没有必要加锁的,这种场景使用读写锁是更好的一种选择,可以提高性能。
读写锁分两种:读锁
和写锁
。当一个 goroutine
获取读锁之后,其他的 goroutine
如果是获取读锁会继续获得锁,如果是获取写锁就会等待;当一个 goroutine
获取写锁之后,其他的 goroutine
无论是获取读锁还是写锁都会等待。
//实验不加锁、使用互斥锁、使用读写互斥锁的区别
//不加锁最快,使用互斥锁最慢
package main
import (
"fmt"
"sync"
"time"
)
var x int64
var sw sync.WaitGroup
// 使用互斥锁
var lock sync.Mutex
// 使用读写互斥锁
var rwlock sync.RWMutex
func read() {
defer sw.Done()
rwlock.RLock() // 读互斥加锁
// lock.Lock() // 互斥加锁
time.Sleep(time.Millisecond * 1)
// lock.Unlock() // 互斥解锁
rwlock.RUnlock() // 读互斥解锁
}
func write() {
defer sw.Done()
rwlock.Lock() // 写互斥加锁
// lock.Lock() // 互斥锁加锁
x++
time.Sleep(time.Millisecond * 5)
// lock.Unlock() // 互斥锁解锁
rwlock.Unlock() // 写锁解锁
}
func main() {
start := time.Now()
// 写入 10 次
for i:=0;i<10;i++ {
sw.Add(1)
go write()
}
// 读取 1000 次
for i:=0;i<1000;i++ {
sw.Add(1)
go read()
}
sw.Wait()
end := time.Now()
fmt.Printf("用时:%v.\n",end.Sub(start))
}
/*
用时:58.3229ms.
*/
1.2.3 sync.Once
延迟
一个开销很大的初始化操作,到真正用到它的时候再执行,例如:定义了一个 init
初始化函数,程序启动的时候会被自动加载,无论是否用到都会加载,这样程序就会增加程序的启动延时。
sync.Once
是 Go 中提供的一个同步原语,确保某个操作只执行一次。无论这个操作被调用多少次,sync.Once
只会保证它的 Do 方法中的函数只执行一次
关键点:
并发安全
:sync.Once
是并发安全的,它可以被多个 goroutine 安全地调用。在多个goroutine
同时调用 Do 方法时,只有第一次调用会执行传入的函数,后续调用会被忽略。只执行一次
:无论调用多少次,传入的函数只会执行一次。
sync.Once
只有一个 Do 方法,示例:
// 定义 sync.Once
var onlyOne sync.Once
func initDatabase() {
fmt.Println("Database initialized!")
}
// 被多个 goroutine 调用时不是并发安全的
func card(name string) {
if cards == nil {
onlyOne.Do(initDatabase)
}
}
示例:使用 sync.Once 调用带参函数
package main
import (
"fmt"
"sync"
)
// 定义 sync.Once
var onlyOne sync.Once
func test(x int) {
fmt.Println(x)
}
// 闭包
func closer(x int) func() {
return func() {
test(x)
}
}
func main() {
t := closer(10)
onlyOne.Do(t)
}
/*
10
*/
1.2.4 sync.Map
sync.Map
是 Go 语言标准库中提供的一个并发安全
的映射(字典)实现,它用于在并发环境中处理共享数据。与普通的 map
不同,sync.Map
为多 goroutine
提供了内建的同步机制,避免了传统的 map
在并发读写时可能引发的数据竞态问题。
sync.Map
是专为并发设计的,它内部使用了一些优化,使得它在高并发场景下的表现优于使用 sync.Mutex
或 sync.RWMutex
手动加锁的方案。
sync.Map 的特点:
并发安全
:sync.Map
的所有操作(读、写、删除)在多个 goroutine 中是并发安全的,避免了使用普通的 map 时可能遇到的竞态条件。高效
:在读多写少的场景下,sync.Map
的性能表现较好。它使用了内部分段的锁和无锁的操作,以优化性能。不支持直接迭代
:不像传统的 map,sync.Map 不支持直接通过 range 来遍历,因此没有原生的顺序性。支持原子操作
:sync.Map 提供了原子级别的操作,使得并发访问数据时不会发生冲突。
常用方法:
- Store(key, value):
用于向 sync.Map 中存储键值对。如果键已存在,则覆盖原有的值。 - Load(key):
用于获取sync.Map
中指定键的值。返回两个值,第二个值是一个布尔值,表示键是否存在。 - LoadOrStore(key, value):
如果 key 已存在,返回现有值和 true,否则存储新的值并返回 false。 - Delete(key):
用于删除 sync.Map 中指定的键值对。 - Range(f func(key, value interface{}) bool):
遍历 sync.Map 中所有的键值对。与普通的 map 不同,sync.Map
不支持range
遍历,需要通过Range
方法进行遍历。
Range
接受一个回调函数 f
,这个函数会被每个键值对调用。如果回调函数返回false
,则遍历会停止。
m.Store("key1", "value1")
m.Store("key2", "value2")
m.Range(func(key, value interface{}) bool {
fmt.Println(key, value)
return true
})
示例
package main
import (
"fmt"
"sync"
)
func main() {
var m sync.Map
// Store一些键值对
m.Store("name", "John")
m.Store("age", 30)
// Load读取值
if value, ok := m.Load("name"); ok {
fmt.Println("name:", value)
}
// LoadOrStore 如果存在则返回现有值,不存在则存储新的值
value, loaded := m.LoadOrStore("age", 40)
fmt.Println("age:", value, "loaded:", loaded)
// Delete 删除键值对
m.Delete("name")
// Range遍历
m.Range(func(key, value interface{}) bool {
fmt.Println(key, value)
return true
})
}
1.3 Context
1.3.1 简介
context
是一个用于跨 API 边界传递元数据、取消信号、超时控制以及其他请求范围内的信息的包。它常用于处理并发编程中的超时、取消、以及请求范围内的数据传递。
那么为什么需要 context?
在并发编程中,当多个协程运行时,可能会遇到以下问题:
- 请求取消:在某些情况下,可能需要在某个请求被取消时终止正在进行的操作。
- 超时控制:有时需要设置操作的超时时间,超过时间就终止操作。
- 跨 API 边界传递元数据:需要在不同的函数、服务或系统之间传递共享的状态信息。
为了处理这些问题,context 被引入 Go 语言。通过 context,你可以更好地管理和控制协程的生命周期,特别是在复杂的并发程序中。
context 的常见用途:
- 取消操作:context 可以传递取消信号,能够在不再需要某个操作时立即停止它。
- 设置超时:通过
context.WithTimeout()
或context.WithDeadline()
设置操作的超时限制。 - 传递共享数据:context 可以携带元数据,如请求 ID 或认证信息,供多个函数调用共享。
1.3.2 主要功能
context接口
type Context interface {
// 获取设置的截止时间:
// 第一个返回值是截止时间,到了这个时间点,Context 会自动发起取消请求;
//第二个返回值 ok==false 时表示没有设置截止时间,如果需要取消的话,需要调用取消函数进行取消
Deadline() (deadline time.Time, ok bool)
// 该方法返回一个只读的 chan,类型为 struct{},如果该方法返回的 chan 可以读取,则意味着parent context已经发起了取消请求,我们通过 Done 方法收到这个信号后,就应该做清理操作,然后退出 goroutine,释放资源。
Done() <- chan struct { }
// 返回取消的错误原因,因为什么 Context 被取消。
Err() error
// 获取该 Context 上绑定的值,是一个键值对,所以要通过一个 Key 才可以获取对应的值,这个值一般是线程安全的。
Value(key interface{}) interface{ }
}
context 包提供了以下几种常见功能:
- 创建根 context(背景 Context)
ctx := context.Background()
context.Background()
是一个返回空context
的函数,通常用作根 context,在应用程序的入口点被调用,用于所有后续的 context 创建和操作。 - 创建带取消信号的 Context
ctx, cancel := context.WithCancel(parentContext)
context.WithCancel
创建一个可以被取消的 context。cancel 是一个函数,调用它可以取消 context,通知所有通过该 context 传递的操作取消。 - 设置超时的 Context
ctx, cancel := context.WithTimeout(parentContext, 5*time.Second)
context.WithTimeout
创建一个有超时限制的 context,如果超时,context 会自动被取消。 - 设置具体的截止时间
context.WithDeadline
创建一个有具体截止时间的 context,在指定的时间点自动取消 context。
deadline := time.Now().Add(5 * time.Second)
ctx, cancel := context.WithDeadline(parentContext, deadline)
- 传递值的 Context
ctx := context.WithValue(parentContext, "key", "value")
context.WithValue
允许在 context 中传递一些值。这些值可以是请求上下文中的元数据,例如请求 ID、用户信息等。
1.3.3 使用示例
1.3.3.1 取消信号
package main
import (
"context"
"fmt"
"time"
)
func longRunningTask(ctx context.Context) {
select {
case <-time.After(10 * time.Second):
fmt.Println("Task completed")
case <-ctx.Done():
fmt.Println("Task cancelled")
}
}
func main() {
ctx, cancel := context.WithCancel(context.Background())
go longRunningTask(ctx)
// 模拟 3 秒后取消任务
time.Sleep(3 * time.Second)
// 使用context 的cancel 函数停止goroutine
cancel()
// 给任务足够时间打印结果
time.Sleep(1 * time.Second)
}
输出:
Task cancelled
在这个例子中,longRunningTask 函数执行一个长时间运行的操作(假设是 10 秒)。但是,主函数会在 3 秒后调用 cancel(),使得任务提前被取消,ctx.Done() 通知任务取消。
1.3.3.2 设置超时
package main
import (
"context"
"fmt"
"time"
)
func longRunningTask(ctx context.Context) {
select {
case <-time.After(10 * time.Second):
fmt.Println("Task completed")
case <-ctx.Done():
fmt.Println("Task timed out")
}
}
func main() {
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
go longRunningTask(ctx)
// 等待 6 秒,让超时发生
time.Sleep(6 * time.Second)
}
输出:
Task timed out
这里,我们设置了一个 5 秒的超时,任务将在 5 秒后自动取消。
1.3.3.3 传递值
package main
import (
"context"
"fmt"
)
func main() {
ctx := context.WithValue(context.Background(), "user_id", 1234)
printUserInfo(ctx)
}
func printUserInfo(ctx context.Context) {
userID := ctx.Value("user_id")
fmt.Println("User ID:", userID)
}
输出:
User ID: 1234
在这个示例中,context.WithValue 将一个键值对(user_id 和 1234)存储到 context 中。然后,通过 ctx.Value("user_id") 获取这个值。
标签:func,Context,互斥,fmt,sync,并发,context,Go From: https://www.cnblogs.com/jingzh/p/18638388