Golang中如何通过channel实现资源通信?

Golang中如何通过channel实现资源通信? 你好…

能否请你解释一下这句话?

“不要通过共享内存来通信;相反,要通过通信来共享内存。”

我来自基于线程的编程范式,该范式使用锁来竞争共享资源…我仍然对上述说法如何解决这个问题感到困惑…我真的需要理解这一点才能充分利用 Go 的并发模型…我想象有 8 个线程在竞争一个共享资源,所以所有线程都会等待,只有一个线程可以锁定共享资源,而其他 7 个线程将等待…

我想理解上述说法如何能在无竞争的情况下共享数据…我仍然无法理解…你能给我一个简单的代码示例,展示如何在无竞争的情况下共享数据吗?(在 goroutine 之间)

c/c++ 背景

这是 Go 中最重要的概念,我想理解它…

此致


更多关于Golang中如何通过channel实现资源通信?的实战教程也可以访问 https://www.itying.com/category-94-b0.html

4 回复

当通过"共享内存进行通信"时,存在对共享内存的并发访问,您必须努力保护这种访问。这就是您提到的8个线程的例子。当通过"通信共享内存"时,您将值(在内存中,因此您共享了内存)发送到通道(通信)。因此这些发送操作通过通道自然地被序列化,接收方所做的任何修改都将是原子性的、并发安全的(如果正确实现)。

更多关于Golang中如何通过channel实现资源通信?的实战系列教程也可以访问 https://www.itying.com/category-94-b0.html


“你在传递值(在内存中,所以你共享了内存)”

这一点很重要,所以你说仅仅传递值就不会产生竞争…但如果我也想进行修改呢?我能通过通道传递引用吗?或者这不是正确的做法(可以把它想象成传递状态)…你能分享一个示例链接吗?我会尝试研究代码…从基于线程的模型转变思维确实很困难,就像你一生都习惯于这种思维模式,当有人告诉你存在更好的方法时…你很难彻底理解它…

如果你将原始值的指针传递给通道,可能会遇到竞态问题。请记住,当你传递指针或引用时,实际上是在发布它,你无法预知这个"属于自己"的对象会经历多少次修改(实际上,一旦发布它就不再属于你了)。更严重的是,接收方若编写了不当的并发代码来修改你的对象,可能导致灾难性后果(别忘了,一旦你提供引用,对方就能并发修改你的对象)。因此最佳实践是传递副本(Go语言天然支持这一特性)。若需查看具体示例,请参考生产者/消费者问题:http://www.golangpatterns.info/concurrency/producer-consumer

祝您有美好的一天。

在Go语言中,“不要通过共享内存来通信;相反,要通过通信来共享内存” 这一理念是并发编程的核心。让我通过具体代码示例来解释这个概念。

传统共享内存方式(有竞争)

在基于锁的编程中,多个goroutine直接访问共享变量:

package main

import (
    "fmt"
    "sync"
    "time"
)

// 传统方式:使用锁保护共享内存
func traditionalSharedMemory() {
    var counter int
    var mutex sync.Mutex
    var wg sync.WaitGroup
    
    for i := 0; i < 8; i++ {
        wg.Add(1)
        go func(id int) {
            defer wg.Done()
            mutex.Lock()
            // 临界区 - 只有一个goroutine能进入
            counter++
            fmt.Printf("Goroutine %d: counter = %d\n", id, counter)
            mutex.Unlock()
        }(i)
    }
    
    wg.Wait()
    fmt.Printf("Final counter: %d\n", counter)
}

这种方式确实存在竞争,所有goroutine都在等待锁。

Go的方式:通过通信共享内存

package main

import (
    "fmt"
    "time"
)

// Go方式:通过channel通信来共享数据
func goStyleCommunication() {
    // 创建一个channel用于通信
    counterChan := make(chan int)
    done := make(chan bool)
    
    // 启动一个goroutine来管理共享状态
    go func() {
        counter := 0
        for {
            select {
            case op := <-counterChan:
                // 接收操作请求,安全地更新计数器
                counter += op
                fmt.Printf("Counter updated: %d\n", counter)
            case <-done:
                return
            }
        }
    }()
    
    // 启动8个goroutine,通过channel发送操作请求
    for i := 0; i < 8; i++ {
        go func(id int) {
            // 发送增量操作,而不是直接访问共享变量
            counterChan <- 1
            fmt.Printf("Goroutine %d sent increment request\n", id)
        }(i)
    }
    
    // 等待所有操作完成
    time.Sleep(1 * time.Second)
    done <- true
    close(counterChan)
}

更实用的示例:工作池模式

package main

import (
    "fmt"
    "sync"
)

type Resource struct {
    data int
}

func worker(id int, jobs <-chan int, results chan<- int, wg *sync.WaitGroup) {
    defer wg.Done()
    for job := range jobs {
        // 处理任务,没有竞争
        result := job * 2
        fmt.Printf("Worker %d processed job %d, result: %d\n", id, job, result)
        results <- result
    }
}

func main() {
    const numWorkers = 4
    const numJobs = 8
    
    jobs := make(chan int, numJobs)
    results := make(chan int, numJobs)
    var wg sync.WaitGroup
    
    // 启动worker池
    for i := 1; i <= numWorkers; i++ {
        wg.Add(1)
        go worker(i, jobs, results, &wg)
    }
    
    // 发送任务
    for j := 1; j <= numJobs; j++ {
        jobs <- j
    }
    close(jobs)
    
    // 收集结果
    go func() {
        wg.Wait()
        close(results)
    }()
    
    // 处理结果
    var total int
    for result := range results {
        total += result
    }
    
    fmt.Printf("Total result: %d\n", total)
}

关键区别

  1. 传统方式:多个goroutine直接竞争访问共享变量
  2. Go方式:只有一个goroutine拥有数据所有权,其他goroutine通过channel发送操作请求

在Go的方式中:

  • 数据所有权明确
  • 没有锁竞争
  • 通过消息传递协调
  • 更易于理解和维护

这种方式消除了数据竞争,因为只有一个goroutine实际访问数据,其他goroutine只是发送操作请求。

回到顶部