package main
import (
"fmt"
)
func main() {
var i int
ch := make(chan struct{})
chs := make(chan struct{})
maps := make(map[int]string,0)
go func(ch chan struct{}) {
i = 10
maps[i] = "aa"
fmt.Printf("map结果:%s\n", maps[i])
close(ch)
}(ch)
go func(ch chan struct{},chs chan struct{}) {
<-ch
i += 10
maps[i] = "bb"
fmt.Printf("map结果:%s\n", maps[i])
close(chs)
}(ch,chs)
<-chs
fmt.Printf("最终结果:%d\n", i) // 输出20
}
我在测试两个协程写入一个共享变量和操作map(读写),我运行了几十万次,也没有发现异常,是不是使用channel对协程互斥,就不用对全局变量i、maps加锁了?
golang 使用channel做协程互斥,是不是就用加锁
- 写回答
- 好问题 0 提建议
- 关注问题
- 邀请回答
-
4条回答 默认 最新
Juan_2012 2025-10-29 19:26关注回答参考qwen.
你提出的问题非常关键,涉及 Go 语言并发编程的核心思想:“不要通过共享内存来通信,而是通过通信来共享内存”。
你的代码虽然在几十万次运行中没有出现异常,但这 不等于它是线程安全的或正确的做法。下面我们来深入分析。
🔍 你的代码逻辑解析
var i int ch := make(chan struct{}) chs := make(chan struct{}) maps := make(map[int]string, 0) // 协程1:设置 i=10,写 maps[10]="aa",然后 close(ch) go func(ch chan struct{}) { i = 10 maps[i] = "aa" fmt.Printf("map结果:%s\n", maps[i]) close(ch) }(ch) // 协程2:等待 <-ch 后执行 i += 10,maps[20]="bb" go func(ch, chs chan struct{}) { <-ch // 等待第一个协程完成 i += 10 maps[i] = "bb" fmt.Printf("map结果:%s\n", maps[i]) close(chs) }(ch, chs) <-chs fmt.Printf("最终结果:%d\n", i)✅ 输出是稳定的
20,是因为:- 两个 goroutine 是串行执行的:
- 第二个协程必须等到第一个
close(ch)才能继续
- 第二个协程必须等到第一个
- 没有并发读写,所以看起来“安全”
✅ 回答核心问题:
使用 channel 做协程同步,是否就可以不用对全局变量加锁?
✔️ 正确答案是:
如果你用 channel 成功地避免了多个 goroutine 同时访问共享数据(即实现了“同一时间只有一个协程能操作变量”),那么就不需要额外加锁。
这正是你这个例子的情况 —— 你通过 channel 实现了互斥访问,而不是多个 goroutine 并发访问。
🧠 类比理解:Channel 是一种“隐式锁”
方式 特点 sync.Mutex显式加锁/解锁,允许多个协程竞争进入临界区 channel控制执行顺序隐式互斥,只有收到信号的协程才能继续,本质是控制权传递 👉 所以你说得没错:在这种场景下,不需要再给
i或maps加锁,因为根本就没有并发!
⚠️ 但要注意:这不是“用 channel 替代锁”,而是“用 channel 避免并发”
❌ 错误认知:
“只要用了 channel,随便怎么访问共享变量都安全。”
✅ 正确认知:
“我通过 channel 安排了执行顺序,确保任何时候最多只有一个协程在访问共享资源,因此无需锁。”
🧪 举个反例:即使用了 channel,仍然可能出错
ch := make(chan int) go func() { for i := 0; i < 1000; i++ { globalCounter++ } ch <- 1 }() go func() { for i := 0; i < 1000; i++ { globalCounter++ } ch <- 1 }() <-ch <-ch📌 虽然用了 channel 同步,但两个 goroutine **并发修改
globalCounter**,这就是典型的竞态条件(race condition),会导致结果不确定。此时就必须:
- 加
sync.Mutex - 或改用原子操作
atomic.AddInt64 - 或完全通过 channel 传递状态(见下文推荐模式)
✅ 推荐的 Go 并发模式:通过 channel 共享内存
真正体现 Go 哲学的做法是:不让多个 goroutine 直接访问共享变量,而是由一个 goroutine 拥有它,其他通过 channel 发送消息来操作它。
示例:用 channel 实现安全计数器
type Op struct { op string // "inc" or "get" res chan int } func counter() { var value int ops := make(chan Op) go func() { for op := range ops { switch op.op { case "inc": value++ case "get": op.res <- value } } }() // 使用示例 ops <- Op{op: "inc"} ops <- Op{op: "inc"} res := make(chan int) ops <- Op{op: "get", res: res} fmt.Println("Counter:", <-res) // 安全获取 }📌 这才是 Go 官方提倡的:“**Do not communicate by sharing memory; share memory by communicating.**”
📝 总结
问题 回答 我的代码几十万次没出错,是不是安全? ✅ 是安全的,因为你用 channel 保证了串行化 是否还需要对 i和maps加锁?❌ 不需要,因为没有并发访问 使用 channel 就可以替代锁吗? ⚠️ 不完全是 —— 只有当 channel 能确保互斥访问时才可以 如何写出更地道的 Go 并发代码? ✅ 让一个 goroutine 独占数据,其余通过 channel 与其通信
本回答被题主选为最佳回答 , 对您是否有帮助呢?解决 无用评论 打赏 举报- 两个 goroutine 是串行执行的: