当前位置: 首页 > news >正文

第六章:并发编程—Go的杀手锏

在这里插入图片描述


上一章:《第五章:Go的“面向对象”编程》


文章目录

  • 1.核心概念:从Java线程到Go协程
  • 2.Goroutine:轻量级线程
  • 3.Channel:Goroutine间的通信管道
    • 3.1 Channel的创建与使用
    • 3.2 Channel方向
    • 3.3 多个Goroutine通信
    • 3.4 Channel的关闭和检测
    • 3.5 生产者-消费者模式
    • 3.6 Channel超时处理
  • 4.同步原语:sync包
    • 4.1 sync.WaitGroup:等待一组Goroutine完成
    • 4.2 sync.Mutex:互斥锁
      • Demo 1: 无锁场景(竞态条件示例)
      • Demo 2: 基础互斥锁使用(解决竞态条件)
      • Demo 3: 互斥锁常见注意事项(避免死锁)
  • 5.实战模式:常用并发模式
  • 6.核心哲学:通过通信来共享内存
  • 7.总结

1.核心概念:从Java线程到Go协程

在深入之前,让我们先理解基本概念和差异:

特性Java (传统线程模型)Go (CSP并发模型)
基本单位线程(Thread),内核态,重量级(~1MB+)协程(Goroutine),用户态,极轻量(~2KB)
创建成本高,数量受限(千级别)极低,可轻松创建百万个
通信方式基于共享内存,需通过锁(synchronized, Lock)同步基于消息传递,通过 Channel 通信
调度方式操作系统内核调度,上下文切换成本高Go 运行时(runtime)自身调度,上下文切换成本极低
哲学通过共享内存来通信通过通信来共享内存

虽然Java21之后默认支持虚拟线程,可以轻松的创建和销毁,但两者之间还是有很多性能的差异

对比维度Go (CSP并发模型)Java (虚拟线程)关键差异
核心理念通信顺序进程(Communicating Sequential Processes)
“不要通过共享内存来通信,而应该通过通信来共享内存”
基于线程的轻量级并发
延续传统线程模型,但降低开销
Go强调“通过通信共享内存”,Java仍基于“共享内存 + 同步”
并发单位Goroutine(轻量级协程)
由Go运行时管理,栈初始仅2KB,可动态增长
虚拟线程(Virtual Threads)
由JVM管理,栈大小固定但开销远低于平台线程
两者都是轻量级,但Go的Goroutine更轻(栈更小,调度更激进)
创建成本极低
go func() 可轻松创建数万Goroutine
低(相比平台线程)
但仍高于Goroutine,受限于JVM内存管理
Go创建成本更低,更适合高并发短任务(如Web服务器)
调度机制M:N 调度(G-P-M模型)
Goroutine由Go运行时在少量OS线程上多路复用
平台线程 + 虚拟线程协作
虚拟线程绑定到平台线程执行(Fork-Join池)
Go是用户态完全调度,Java是平台线程托管执行
通信方式通道(Channel)
通过 chan 进行同步/异步通信,支持 select 多路复用
共享内存 + 同步原语
使用 volatilesynchronizedLockAtomic
Go用通道通信,Java用锁和原子操作同步
错误处理panic / recover 机制
Goroutine崩溃可被捕获,不直接导致整个程序退出
异常传播
虚拟线程中的异常需显式处理或通过 Thread.UncaughtExceptionHandler
Go的 recover 可局部恢复,Java异常需外部捕获
阻塞处理非阻塞I/O自动调度
当Goroutine阻塞(如网络I/O),Go运行时自动调度其他Goroutine
需显式使用 Structured Concurrency 或 try-with-resources
阻塞时平台线程被占用
Go对阻塞更友好,Java需注意平台线程饥饿
编程范式基于通道的数据流编程
典型模式:生产者-消费者、扇入扇出
基于线程的任务并行
典型模式:ExecutorServiceForkJoinPool
Go更偏向函数式/响应式风格,Java更偏向命令式/任务化
调试与监控支持 pprof 分析Goroutine泄漏
可通过 runtime.NumGoroutine() 监控
可通过 jstack、JFR 监控虚拟线程
支持 Thread.startVirtual() 创建
Java工具链更成熟,Go更简洁
典型场景高并发网络服务(如API网关、微服务)
大量短生命周期任务
传统企业应用迁移

高吞吐I/O密集型应用(如数据库连接池)
Go适合云原生新项目,Java适合遗留系统升级

两者并非互斥,而是不同哲学下的演进路径:Go从一开始就为并发设计,Java则在传统模型上做轻量化升级。

  1. 哲学差异
    Go:“通过通信共享内存”
    并发单元(Goroutine)通过通道(Channel)传递数据,避免共享状态,天然减少竞态条件。
    Java:“共享内存 + 显式同步”
    虚拟线程仍共享堆内存,需通过锁、原子操作等机制保护共享状态。
  2. 调度粒度
    Go:M:N 调度,Go运行时可在单个OS线程上调度成千上万个Goroutine,切换开销极小。
    Java:1:1 或 M:1 调度,虚拟线程最终仍绑定到平台线程执行,若大量阻塞I/O可能导致平台线程饥饿。
  3. 编程复杂度
    Go:通道和select让并发逻辑清晰,但需学习新范式。
    Java:对熟悉传统线程的开发者更友好,但需小心处理死锁、活锁等问题。
  4. 生态系统
    Go:标准库内置context、sync、channel,生态统一。
    Java:虚拟线程是Project Loom的一部分,需Java 19+,且需与现有ExecutorService等兼容。

​​思维转换​​:在Go中,你不再需要小心翼翼地管理线程池和纠结于复杂的锁机制。你可以像创建普通变量一样创建成千上万个并发任务,让Go运行时去高效地调度它们。

2.Goroutine:轻量级线程

Goroutine是Go并发模型的基本执行单元。它由Go运行时管理,而不是操作系统。

如何启动一个Goroutine?

只需在函数调用前加上关键字 go即可。

package mainimport ("fmt""time"
)func say(s string) {for i := 0; i < 5; i++ {time.Sleep(100 * time.Millisecond)fmt.Printf("%s %d\n", s, i+1) // 修复:使用Printf格式化输出}
}func main() {//同步调用say("同步调用")//异步调用go say("异步调用1")go say("异步调用2")time.Sleep(1000 * time.Millisecond)fmt.Println("main函数结束")}

在这里插入图片描述

​​与Java对比​​:

// Java中实现类似功能需要创建Thread或使用线程池
new Thread(() -> {for (int i = 0; i < 3; i++) {try { Thread.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); }System.out.println("异步:World");}
}).start();

3.Channel:Goroutine间的通信管道

Channel是连接并发Goroutine的管道,你可以通过它发送和接收值。这是Go实现“通过通信来共享内存”的核心。

3.1 Channel的创建与使用

Channel使用make创建,并通过<-操作符进行发送和接收。

func main() {// 创建一个字符串类型的Channelmessages := make(chan string)// 启动一个Goroutine,向Channel发送消息go func() {time.Sleep(1 * time.Second)messages <- "ping" // 发送消息"ping"到Channel}()// 从Channel接收消息(这会阻塞,直到有消息到来)msg := <-messagesfmt.Println(msg) // 输出: ping// 默认发送和接收是阻塞的,这使得同步可以在没有锁的情况下完成
}

有缓冲 vs 无缓冲 Channel

类型行为使用场景
无缓冲Channel make(chan T)发送和接收必须同时准备好,否则阻塞。是同步的。用于强同步的Goroutine间通信
有缓冲Channel make(chan T, size)缓冲区满时发送阻塞,缓冲区空时接收阻塞。是异步的。用于生产者和消费者模型,解耦并发任务
ackage mainimport ("fmt"
)// ========== Channel基础概念 ==========func basicChannelDemo() {fmt.Println("=== 基础Channel演示 ===")// 创建一个int类型的channelch := make(chan int)// 启动goroutine发送数据go func() {fmt.Println("发送数据: 42")ch <- 42 // 发送数据到channelfmt.Println("数据已发送")}()// 主goroutine接收数据fmt.Println("等待接收数据...")value := <-ch // 从channel接收数据fmt.Printf("接收到数据: %d\n", value)
}// ========== 带缓冲的Channel ==========func bufferedChannelDemo() {fmt.Println("\n=== 带缓冲Channel演示 ===")// 创建缓冲大小为3的channelch := make(chan string, 3)// 发送数据(不会阻塞,因为有缓冲区)ch <- "Apple"ch <- "Banana"ch <- "Cherry"fmt.Printf("Channel长度: %d, 容量: %d\n", len(ch), cap(ch))// 接收数据for i := 0; i < 3; i++ {fruit := <-chfmt.Printf("接收到: %s\n", fruit)}
}

3.2 Channel方向


// 只能发送的channel
func sender(ch chan<- int, start int) {for i := start; i < start+5; i++ {fmt.Printf("发送: %d\n", i)ch <- itime.Sleep(100 * time.Millisecond)}close(ch) // 关闭channel
}// 只能接收的channel
func receiver(ch <-chan int, name string) {for value := range ch { // 使用range遍历channelfmt.Printf("%s 接收到: %d\n", name, value)}fmt.Printf("%s 完成接收\n", name)
}func main() {fmt.Println("\n=== Channel方向演示 ===")ch := make(chan int, 2)go sender(ch, 1)go receiver(ch, "Receiver-1")time.Sleep(1 * time.Second)
}

在这里插入图片描述

上面这段代码是一个经典的生产者-消费者模式,这是Go并发编程中最基础、最常用的设计模式之一。这种模式在实际应用中非常广泛,例如:

  • 任务队列:多个生产者(如不同的Goroutine)向队列发送任务,而消费者(如一个Goroutine)从队列中取出任务并处理。
  • 数据管道:如HTTP服务器处理请求时,将请求数据通过Channel传递给处理函数。
  • 并发计算:如MapReduce模型,将数据分布到多个Goroutine处理,最后将结果合并。

代码特点

  • 单向channel:sender使用chan<- int(只发送),receiver使用<-chan int(只接收),这是一种良好的实践
  • 带缓冲的channel:make(chan int, 2)创建了容量为2的channel
  • channel关闭:sender在发送完所有数据后关闭channel,使receiver的range循环能正常结束
  • 生产者-消费者模型:sender是生产者,receiver是消费者

下面继续针对Go的Channel进行补充说明:

在Go中,receiver(接收者)只有在sender(发送者)发送了数据后才会被触发。这是Go Channel工作原理的核心特性。

在上面的这段代码中:

ch := make(chan int, 2)  // 创建一个容量为2的带缓冲Channel
go sender(ch, 1, 5)     // 启动sender goroutine
go receiver(ch, "Receiver-1")  // 启动receiver goroutine

具体工作流程如下:

  • channel初始化:创建了一个容量为2的带缓冲channel
    • make(chan int, 2):可以存储2个int值
  • goroutine启动:
    • go sender(…):启动sender goroutine,它会开始向channel发送数据
    • go receiver(…):启动receiver goroutine,它会等待从channel接收数据
  • channel工作原理:
    • 当receiver开始执行for value := range ch时,它会尝试从channel接收数据
    • 如果channel中没有数据,receiver会阻塞(等待),直到有数据可接收
    • sender会先发送数据到channel(直到缓冲区满或接收方准备好)
    • 当sender发送了第一个数据时,receiver就会开始接收数据

这种"等待数据到达"的设计是Go并发编程的核心优势,它:

  • 避免了轮询(busy waiting)
  • 保证了数据同步
  • 简化了并发控制逻辑

3.3 多个Goroutine通信

func worker(id int, jobs <-chan int, results chan<- int) {for job := range jobs {fmt.Printf("Worker %d 开始处理 job %d\n", id, job)time.Sleep(time.Duration(rand.Intn(1000)) * time.Millisecond)results <- job * 2 // 处理结果fmt.Printf("Worker %d 完成处理 job %d\n", id, job)}
}func main() {fmt.Println("\n=== Worker Pool演示 ===")const numJobs = 5const numWorkers = 3jobs := make(chan int, numJobs)results := make(chan int, numJobs)// 启动workersfor w := 1; w <= numWorkers; w++ {go worker(w, jobs, results)}// 发送jobsfor j := 1; j <= numJobs; j++ {jobs <- j}close(jobs)// 收集结果for a := 1; a <= numJobs; a++ {result := <-resultsfmt.Printf("结果: %d\n", result)}
}

在这里插入图片描述

这段代码是一个典型的Worker Pool(工作池)模式的实现,用于并发处理任务。让我们一起详细分析一下:

work函数做的事情:

  • worker函数接收三个参数:
    • id: worker的标识
    • jobs: 任务通道(只读),从这个通道中接收任务
    • results: 结果通道(只写),将处理结果发送到这个通道
  • 函数内部使用for job := range jobs循环,等待从jobs通道接收任务
  • 每个任务处理时会随机休眠一段时间(模拟处理时间)
  • 处理完成后将结果(job * 2)发送到results通道

main方法里做的事情则是:

  • 启动3个worker goroutine
  • 将5个任务(1-5)发送到jobs通道
  • 关闭jobs通道(表示任务已全部发送完毕)
  • 从results通道接收并打印5个结果

读到此处,如果之前是从事Java的同学不难发现这个和我们经典的ThreadPoolExecutors设计有着异曲同工之妙,但是却更加的轻量和优雅

工作池模式的核心思想

  • 固定数量的worker:预先创建一定数量的worker goroutine,而不是为每个任务都创建一个新的goroutine
  • 任务队列:任务通过channel发送到任务队列,worker从队列中取任务处理
  • 结果返回:worker处理完成后将结果发送到结果通道

了解其中的大致流程之后,我们可以还注意到运行结果出现了死锁,原因是:未正确关闭结果通道(results),这将导致程序在接收完所有结果后永久阻塞,无法正常退出

在Go中,for range循环必须在通道被关闭后才会结束。代码中:

  • 创建了results通道(容量为jobNums=5)
  • 但从未关闭这个通道
  • 当所有5个结果被接收后,for range循环会继续等待(因为通道未关闭),导致程序永久阻塞

解决方式也很简单,我们只需要控制通道结束时进行通知 ,待所有的woker完成任务之后关闭通道,然后打印结果即可

package mainimport ("fmt""math/rand""sync""time"
)func worker(id int, jobs <-chan int, results chan<- int) {for job := range jobs {fmt.Printf("worker %d start job %d\n", id, job)time.Sleep(time.Duration(rand.Intn(1000)) * time.Millisecond)results <- job * 2fmt.Printf("worker %d finish job %d\n", id, job)}
}
func main() {const jobNums = 5const workerNums = 3//创建jobs和worksjobs := make(chan int, jobNums)results := make(chan int, jobNums)//启动workerswg := sync.WaitGroup{}wg.Add(workerNums)for i := 0; i < workerNums; i++ {go func(id int) {defer wg.Done()worker(id, jobs, results)}(i)}//发送任务for i := 0; i < jobNums; i++ {jobs <- i}close(jobs)// 等待所有worker完成wg.Wait()close(results) // 关键:关闭结果通道,让range循环正常退出//收集结果for result := range results {fmt.Printf("结果:%d\n", result)}
}

在这里插入图片描述

里面引入了一个新的角色:sync包,这便是我们下一节讲述的重点,但是后面针对Channel的演示demo,会大量的出现sync的同步等待逻辑,所以建议大家先去阅读一下第4节:同步原语:sync包之后在来学习3.3 3.4以及3.5的内容

3.4 Channel的关闭和检测

是Go并发编程中我们一定要记住,通道关闭是显式操作,不是自动的,这个是书写时会经常遇到得非常典型的错误

package mainimport ("fmt"
)func main() {ch := make(chan int, 3)// 发送一些数据然后关闭go func() {for i := 1; i <= 3; i++ {ch <- i}close(ch)fmt.Println("Channel已关闭")}()// 接收数据并检测channel是否关闭for {value, ok := <-chif !ok {fmt.Println("Channel已关闭,退出接收")break}fmt.Printf("接收到: %d\n", value)}// 使用range自动处理关闭的channelfmt.Println("\n使用range接收:")ch2 := make(chan string, 2)go func() {ch2 <- "Hello"ch2 <- "World"close(ch2)}()for msg := range ch2 {fmt.Printf("Range接收: %s\n", msg)}
}

在这里插入图片描述

3.5 生产者-消费者模式

package mainimport ("fmt""math/rand""sync""time"
)func producer(ch chan<- int, wg *sync.WaitGroup, id int) {defer wg.Done()for i := 1; i <= 5; i++ {product := rand.Intn(100)fmt.Printf("Producer %d 生产: %d\n", id, product)ch <- producttime.Sleep(time.Duration(rand.Intn(500)) * time.Millisecond)}
}func consumer(ch <-chan int, wg *sync.WaitGroup, id int) {defer wg.Done()for product := range ch {fmt.Printf("Consumer %d 消费: %d\n", id, product)time.Sleep(time.Duration(rand.Intn(300)) * time.Millisecond)}
}func producerConsumerDemo() {fmt.Println("\n=== 生产者-消费者演示 ===")ch := make(chan int, 10) // 缓冲区大小为10var wg sync.WaitGroupvar wg2 sync.WaitGroup// 启动生产者wg.Add(2)go producer(ch, &wg, 1)go producer(ch, &wg, 2)// 启动消费者wg2.Add(2)go consumer(ch, &wg2, 1)go consumer(ch, &wg2, 2)// 等待生产者完成wg.Wait()// 关闭channel,通知消费者结束close(ch)// 等待消费者完成wg2.Wait()fmt.Println("生产者-消费者演示完成")
}func main() {producerConsumerDemo()
}

执行流程如下:

  • 创建带缓冲的channel
  • 启动生产者,添加到WaitGroup
    • 启动消费者,添加到WaitGroup2
  • 等待所有生产者完成(wg.Wait())
  • 关闭channel(close(ch))
  • 等待所有消费者完成(wg2.Wait())
  • 程序安全退出

在这里插入图片描述

上面的逻辑是有生产者产生的数据会立即被消费者消费,但是发送速度 和消费速度不一致,如果发送过快或者消费过慢都会导致队列(缓冲区)被打满后,发送操作会阻塞,直到Channel中有空间可用为止,这个和我们消息队列的批量发送和消费逻辑有些相似,当消费速度远低于发送度时就会产生大量的消息挤压,但是如果使用传统的Java实现会比较复杂和臃肿,在Go你可以很容易的就能够平衡发送和消费之间的速度差异过大的问题

3.6 Channel超时处理

package mainimport ("fmt""sync""time"
)func send(ch chan<- string) {time.Sleep(5 * time.Second)ch <- "数据终于来了"close(ch)
}func consumer(ch <-chan string, wg *sync.WaitGroup) {defer wg.Done()// 使用select实现超时select {case msg := <-ch:fmt.Printf("接收到: %s\n", msg)case <-time.After(1 * time.Second):fmt.Println("超时了,放弃等待")}
}func timeoutDemo(wgConsumer *sync.WaitGroup) {fmt.Println("\n=== Channel超时演示 ===")ch := make(chan string)// 启动一个可能很慢的goroutinego send(ch)go consumer(ch, wgConsumer)
}
func main() {var wg sync.WaitGroupwg.Add(1)timeoutDemo(&wg)// 等待goroutine完成wg.Wait()
}

4.同步原语:sync包

4.1 sync.WaitGroup:等待一组Goroutine完成

这是Channel之外最常用的同步工具,用于替代Java中的CountDownLatch。
读到此处,无论你是否对Java的CountDownLatch有了解,我这儿都简单给大家介绍一下

  • CountDownLatch:一个或者多个线程,等待其他多个线程完成某件事情之后才能执行;

与之相对的是CyclicBarrier

  • CyclicBarrier:多个线程互相等待,直到到达同一个同步点,再继续一起执行。

举个例子帮助大家理解一下:

  • CountDownLatch:假设老师跟同学约定周末在公园门口集合,等人齐了再发门票。那么,发门票(这个主线程),需要等各位同学都到齐(多个其他线程都完成),才能执行。
  • CyclicBarrier:多名短跑运动员要开始田径比赛,只有等所有运动员准备好,裁判才会鸣枪开始,这时候所有的运动员才会疾步如飞。

下面继续回到本节的主角:sync包

package mainimport ("fmt""sync""time"
)func worker(id int, wg *sync.WaitGroup) {defer wg.Done() // 通知WaitGroup,当前Goroutine完成fmt.Printf("Worker %d 开始工作\n", id)time.Sleep(time.Second) // 模拟工作耗时fmt.Printf("Worker %d 工作完成\n", id)
}func main() {var wg sync.WaitGroup              // 创建WaitGroupfor i := 1; i <= 5; i++ {wg.Add(1)                      // 每启动一个Goroutine,计数器+1go worker(i, &wg)              // 启动Goroutine}wg.Wait()                          // 等待所有Goroutine完成fmt.Println("All workers finished")
}

4.2 sync.Mutex:互斥锁

当需要在多个Goroutine中安全地访问共享资源时,可以使用互斥锁。

package mainimport ("fmt""sync"
)type SafeCounter struct {mu sync.Mutexv  map[string]int
}// 使用锁保证增加操作是原子性的
func (c *SafeCounter) Inc(key string) {c.mu.Lock()defer c.mu.Unlock() // 使用defer确保锁一定会被释放c.v[key]++
}func (c *SafeCounter) Value(key string) int {c.mu.Lock()defer c.mu.Unlock()return c.v[key]
}func main() {c := SafeCounter{v: make(map[string]int)}var wg sync.WaitGroupfor i := 0; i < 1000; i++ {wg.Add(1)go func() {defer wg.Done()c.Inc("somekey")}()}wg.Wait()fmt.Println(c.Value("somekey")) // 输出: 1000
}

是不是很像我们Java中的RetreentLock的lock和unLock
下面根据几个demo深入了解一下

Demo 1: 无锁场景(竞态条件示例)

当多个 goroutine 并发修改共享变量时,会出现竞态条件(结果小于预期):

package mainimport ("fmt""sync"
)func incr(count *int, iterations int) {for i := 0; i < iterations; i++ {*count++}
}func doIncrProcess(count *int, iterations int, wg *sync.WaitGroup) {defer wg.Done()incr(count, iterations)
}
func main() {//共享变量countcount := 0//用于等待所有goroutine完成var wg sync.WaitGroup// 5个goroutinenumGoroutine := 500// 每个goroutine执行1000次自增iterations := 1000// 注册等待的goroutine数量wg.Add(numGoroutine)for i := 0; i < numGoroutine; i++ {go doIncrProcess(&count, iterations, &wg)}wg.Wait()// 预期结果应为 500*1000=500000,但实际结果可能会小于500000fmt.Printf("无锁场景:预期=%d, 实际=%d\n",numGoroutine*iterations, count)
}

在这里插入图片描述

Demo 2: 基础互斥锁使用(解决竞态条件)

通过 sync.Mutex 保证同一时间只有一个 goroutine 能修改共享变量:

package mainimport ("fmt""sync"
)func incr(count *int, iterations int, mu *sync.Mutex) {for i := 0; i < iterations; i++ {mu.Lock()   // 加锁:独占访问共享资源*count++    // 临界区:安全修改共享变量mu.Unlock() // 解锁:释放资源供其他goroutine使用}
}func doIncrProcess(count *int, iterations int, mu *sync.Mutex, wg *sync.WaitGroup) {defer wg.Done()for i := 0; i < iterations; i++ {mu.Lock()   // 加锁:独占访问共享资源*count++    // 临界区:安全修改共享变量mu.Unlock() // 解锁:释放资源供其他goroutine使用}
}
func main() {counter := 0var mu sync.Mutex // 声明互斥锁var wg sync.WaitGroupnumGoroutines := 500iterations := 1000wg.Add(numGoroutines)for i := 0; i < numGoroutines; i++ {doIncrProcess(&counter, iterations, &mu, &wg)}wg.Wait()// 互斥锁保证了每次只有一个goroutine修改counter,结果必定等于预期fmt.Printf("有锁场景:预期=%d, 实际=%d\n", numGoroutines*iterations, counter)
}

在这里插入图片描述

Demo 3: 互斥锁常见注意事项(避免死锁)

sync.Mutex 必须严格遵循 “加锁后必须解锁” 的原则,否则会导致死锁。以下是错误示例和正确用法对比:

  • 错误示例(死锁风险):
// 错误:在循环中使用defer导致解锁延迟,造成死锁
go func() {defer wg.Done()for j := 0; j < iterations; j++ {mu.Lock()defer mu.Unlock() // 问题:defer会在goroutine结束时才执行,导致锁无法释放counter++}
}()
  • 正确示例(安全解锁):
// 正确:每次加锁后立即解锁,避免死锁
go func() {defer wg.Done()for j := 0; j < iterations; j++ {mu.Lock()counter++mu.Unlock() // 关键:在同一循环迭代中完成解锁}
}()

核心总结:

  • sync.Mutex 通过 Lock() 和 Unlock() 实现互斥访问,确保共享资源在同一时间只能被一个 goroutine 修改
  • 无锁并发修改共享变量会导致竞态条件(数据不一致)
  • 必须严格保证 Lock() 和 Unlock() 成对出现,避免死锁(尤其注意循环和函数返回场景)
  • 可通过 go run -race main.go 命令检测代码中的竞态条件

通过以上示例,可以直观看到 sync.Mutex 如何解决并发安全问题,以及使用时的关键注意事项。

5.实战模式:常用并发模式

注意,本章的实战已经在第三小结里已经体现,大家可以自行通过

  • Worker Pool(工作池)
    • 使用有缓冲Channel创建一组Worker(Goroutine)来处理任务,这是控制并发度的经典模式。
  • 生产者-消费者模式

这两个实现在检验一下

6.核心哲学:通过通信来共享内存

让我们通过一个例子来体会Go与Java在并发哲学上的根本区别。
​​
任务​​:多个Goroutine并发地对一个计数器进行增加操作,并保证线程安全。

Java方式(共享内存+锁):

public class Counter {private int value = 0;private final Object lock = new Object();public void increment() {synchronized(lock) {value++;}}public int getValue() {synchronized(lock) {return value;}}
}
// 多个线程共享同一个Counter实例,通过锁来同步访问。

Go方式(通信共享内存):

type Counter struct {// 计数器值不再是直接共享的字段incCh chan int      // 用于发送增加命令的ChannelgetCh chan int      // 用于获取值的Channel
}func NewCounter() *Counter {c := &Counter{incCh: make(chan int),getCh: make(chan int),}go c.run() // 启动一个管理的Goroutinereturn c
}// 管理的Goroutine独占访问计数器值
func (c *Counter) run() {var value intfor {select {case <-c.incCh: // 接收到增加命令value++case c.getCh <- value: // 接收到获取值命令// 值已发送}}
}func (c *Counter) Increment() {c.incCh <- 1 // 发送增加命令
}func (c *Counter) GetValue() int {return <-c.getCh // 发送获取命令并等待结果
}func main() {counter := NewCounter()var wg sync.WaitGroupfor i := 0; i < 1000; i++ {wg.Add(1)go func() {defer wg.Done()counter.Increment()}()}wg.Wait()fmt.Println(counter.GetValue()) // 输出: 1000
}

在Go的方式中:

  • 计数器的真实值value被​​封装​​在管理的Goroutine中,​​没有其他Goroutine能直接访问它​​。
  • 其他Goroutine只能通过向特定的Channel发送命令来间接操作计数器。
  • 所有对共享资源的访问都​​序列化​​在了管理的Goroutine中,​​根本不需要使用锁​​。

这就是“通过通信来共享内存”的精髓:​​将需要共享的数据限制在单个Goroutine中,通过Channel来传递操作指令和结果,从而避免数据竞争​​。

7.总结

通过本章,你已经掌握了Go并发编程的核心:

  • ​​Goroutine​​:极轻量的并发执行体,创建简单,成本极低。
  • ​Channel​​:Goroutine间的通信管道,是同步和数据传递的首选方式。
  • ​同步工具​​:sync.WaitGroup用于等待一组任务完成,sync.Mutex用于保护共享资源的底层访问。
  • 并发模式​​:掌握了Worker Pool和生产者-消费者等常用模式。
  • ​思维转换​​:深刻理解了“通过通信来共享内存”的哲学,并知道如何用它来编写更安全、更清晰的并发代码。

Go的并发模型让你从繁琐的线程管理和复杂的锁机制中解放出来,让你能够更专注于业务逻辑本身。这是Go在现代高并发场景下如此强大的根本原因。

http://www.dtcms.com/a/474576.html

相关文章:

  • 网站建设内部流程图定制开发网站 推广
  • 衡石科技HENGSHI SENSE 6.0:重塑企业级BI的技术范式
  • 西安便宜网站建设品牌网十大品牌排行榜
  • OpenSIPS call_center 模块测试
  • 深度学习周报(10.6~10.12)
  • 易语言实现多文件选择对话框模块详解
  • 电子商务网站建设与综合实践如何翻译wordpress主题
  • Java基础--集合复习知识点
  • spdlog讲解
  • 怎样用vps做网站超级优化
  • 下载接口返回的数据流格式文件
  • 关于网站建设的合同范本正规太原软件开发公司有哪些
  • Python反射机制通俗详解(新手友好版)
  • 网站开发要源码多少钱wordpress 静态资源加速
  • 【多线程】阻塞等待(Blocking Wait)(以Java为例)
  • 公众号做 视频网站商品行情软件下载
  • Kubernetes环境下Nginx代理Nacos服务请求故障诊断
  • Linux 文件权限详解与实操命令
  • 1Docker镜像与容器,目录挂载和卷映射的选择
  • 06_k8s数据持久化
  • c 教学网站开发网页设计尺寸大小规范
  • 第一章:AI大模型基本原理及API应用——第一小节
  • 购物便宜的网站有哪些vivo即将发布的新手机
  • 超级玛丽demo9
  • 汕头站扩建什么时候完成做单屏网站 高度是多少
  • 【Swift】LeetCode 1. 两数之和
  • CI/CD流水线实战:从零搭建到高效部署
  • AprioriFP-Growth算法详解
  • 吕梁网站定制wordpress登录注册页面模板
  • 网站列表页是啥求个网站这么难吗2021年