每日一题:golang并发 mutex、context
题目
对变量执行2000次+1操作
5个协程并发执行,且支持通过 context 取消操作
package test
import (
"context"
"fmt"
"sync"
"testing"
"time"
)
type num struct {
num int
sync.Mutex
sync.WaitGroup
}
func AddWithLock(ctx context.Context, n *num) {
for i := 0; i < 2000; i++ {
select {
case <-ctx.Done():
fmt.Println("ctx time out")
n.Done()
return
default:
n.Lock()
n.num++
n.Unlock()
}
}
n.Done() // 实际上调用 Done() 后,当前上下文会通知它的所有子上下文取消,但它不会影响父 context 或其他兄弟上下文。
}
func TestLock(t *testing.T) {
ctx, cancelFunc := context.WithTimeout(context.Background(), 5*time.Second)
n := &num{
num: 0,
Mutex: sync.Mutex{},
WaitGroup: sync.WaitGroup{},
}
n.WaitGroup.Add(5)
defer cancelFunc()
for i := 0; i < 5; i++ {
go AddWithLock(ctx, n)
}
n.Wait()
fmt.Println(n.num)
}
总结
并发中:注意结构体使用指针而不是副本
如果结构体中包含sync.Mutex 和 sync.WaitGroup这些需要注意:
- num 结构体的复制问题:
num 结构体包含了 sync.Mutex 和 sync.WaitGroup,这两个类型是不安全被复制的,因为 Mutex 和 WaitGroup 是带有状态的对象。将 num 类型传递给 goroutine 时,会复制 num 实例
你应该使用指针传递 num,而不是传递结构体的副本。
go AddWithLock(ctx, &n)
以及 AddWithLock 函数的参数也应该改为 *num:
func AddWithLock(ctx context.Context, n *num) {
-
WaitGroup 的并发修改问题:
在并发场景中,WaitGroup 的 Add() 和 Done() 操作需要在同一个 goroutine 内进行管理。如果你将 WaitGroup 对象作为结构体成员传递给 goroutine,Done() 操作和 Add() 可能会出现并发问题。
修复建议:
使用指针来传递 WaitGroup,保证 WaitGroup 在并发过程中是安全的。这里修改的关键是将 Add(5) 和 Done() 保证正确调用。 -
select 语句中的超时处理:
在你的 AddWithLock 函数中,当 ctx.Done() 被触发时,你调用了 n.Done() 来标记 goroutine 完成。这里是正确的,但由于 n 是结构体的副本,这个操作并不会影响主函数中的 WaitGroup,因此主线程会一直等待直到超时。
mutex底层实现
TODO
channel底层实现及使用注意
TODO
context 传递取消
- 对子上下文:当父 context 被取消时,所有由它创建的子 context 都会受到影响,会接收到取消信号,Done() 会关闭。
- 对子上下文的影响:调用 Done() 后,当前上下文会通知它的所有子上下文取消,但它不会影响父 context 或其他兄弟上下文。
- 上游上下文:父上下文的 Done() 通道只有在父上下文本身调用 Cancel 时才会关闭,不会因为子上下文的 Done() 被关闭而受到影响。
取消原理:
- 一个context被取消后,会将该ctx从其父ctx的children中移除,并且传播取消信号
- 取消时会关闭Done()对应的只读channel,因此在select那边监听这个channel时,一旦取消,会立即返回一个零值,就监听到取消了