showliuzp 2025-10-29 17:33 采纳率: 84.3%
浏览 4
已结题

golang 使用channel做协程互斥,是不是就用加锁


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加锁了?
  • 写回答

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 控制执行顺序隐式互斥,只有收到信号的协程才能继续,本质是控制权传递

    👉 所以你说得没错:在这种场景下,不需要再给 imaps 加锁,因为根本就没有并发!


    ⚠️ 但要注意:这不是“用 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 保证了串行化
    是否还需要对 imaps 加锁?❌ 不需要,因为没有并发访问
    使用 channel 就可以替代锁吗?⚠️ 不完全是 —— 只有当 channel 能确保互斥访问时才可以
    如何写出更地道的 Go 并发代码?✅ 让一个 goroutine 独占数据,其余通过 channel 与其通信

    本回答被题主选为最佳回答 , 对您是否有帮助呢?
    评论
查看更多回答(3条)

报告相同问题?

问题事件

  • 系统已结题 11月6日
  • 已采纳回答 10月29日
  • 创建了问题 10月29日