3.22 Go之CSP:通信顺序进程

3.22 Go之CSP:通信顺序进程

Go的并发模型

  • 多线程共享内存--->javac++等语言中的多线程开发

  • CSP(Communicating sequential processes)--->两个独立并发实体通过channel进行通信的并发模型

值得注意的是:

  1. Go并没有完全实现CSP仅仅只是实现了process--->goroutinechannel这两个概念

  2. 是并发不是并行,并发的程序可以顺序执行,在真正的多核CPU 上才可能真正地同时运行

  3. 并发编程,对共享资源正确访问要精确控制Go则是将共享值通过通道传递

并发编程的核心

同步通信

同步通信在Go中的举例

示例一:

package main

import (
   "fmt"
   "sync"
)

/*
Go并发编程之同步通信
*/
func main() {
   // 声明互斥量
   var mu sync.Mutex

   // 开启协程加锁
   go func() {
       // 打印结果
       fmt.Println("正在加锁!")
       mu.Lock()
  }()

   // 进行解锁
   mu.Unlock()
}
/*
代码分析:
由于 mu.Lock() 和 mu.Unlock() 并不在同一个 Goroutine 中,所以也就不满足顺序一致性内存模型。同时它们也没有其他的同步事件可以参考,也就是说这两件事是可以并发的。
因为可能是并发的事件,所以 main() 函数中的 mu.Unlock() 很有可能先发生,而这个时刻 mu 互斥对象还处于未加锁的状态,因而会导致运行时异常。
*/

解决上述运行异常问题:

package main

import (
   "fmt"
   "sync"
)

/*
修复上午的问题
*/
func main() {
   // 声明一个互斥锁量
   var mu sync.Mutex

   // 先加一层锁
   mu.Lock()

   // 开启一个协程
   go func() {
       fmt.Println("加锁中!")
       // 解锁
       mu.Unlock()
  }()

   // 再次锁定
   mu.Lock()
}
/*
当第二次加锁时会因为锁已经被占用(不是递归锁)而阻塞,main() 函数的阻塞状态驱动后台线程继续向前执行。
后台线程执行到 mu.Unlock() 时解锁,此时打印工作已经完成了,解锁会导致 main() 函数中的第二个 mu.Lock() 阻塞状态取消
此时后台线程和主线程再没有其他的同步事件参考,它们退出的事件将是并发的,在 main() 函数退出导致程序退出时,后台线程可能已经退出了,也可能没有退出。虽然无法确定两个线程退出的时间,但是打印工作是可以正确完成的。
*/
使用无缓存通道实现同步

明确Go内存模型规范:

  1. Go的无缓存通道:通道接收发生在对通道进行发送完成之前

package main

import "fmt"

/*
采用通道的形式进行:
注意Go的内存模型:
1、通道的接收发生在通道的发送之前
*/
func main() {
   // 声明一个完成通道
   done := make(chan int)

   // 开启一个协程
   go func() {
       // 打印内容
       fmt.Println("打印中!")
       // 通道匿名接收
       <-done
  }()

   // 然后再进行通道发送
   done <- 1
}

注意:

对通道的缓存大小太敏感,如果通道有缓存,就无法保证main() 函数退出之前后台线程能正常打印,更好的做法是将通道的发送和接收方向调换一下

使用带缓存通道实现同步

解决办法:

采用带缓冲通道

package main

import "fmt"

/*
采用缓冲通道解决Go内存模型引起的通道接收发送问题
*/
func main() {
   // 声明通道
   done := make(chan int, 1)

   // 开启一个协程
   go func() {
       // 打印
       fmt.Println("打印中!")
       // 此时先往通道当中写入
       done <- 1
  }()

   // 再次从通道当中读取--->注意匿名读取
   <-done
}

解析:

因为Go当中存在带缓冲的通道那么第一次接收发生再缓冲通道填完之后.所以fmt打印语句能够执行

基于带缓存通道的这些特性,故可以扩展打印线程

package main

import "fmt"

/*
使用带缓冲通道实现同步
*/
func main() {
   // 声明一个带缓冲通道
   done := make(chan int, 10)
   
   // 开启带缓冲通道缓冲区数量的协程
   for i := 0; i < cap(done); i++ {
       // 开启协程
       go func() {
           fmt.Println("打印中!")
           // 写入通道
           done <- 1
      }()
  }

   // 依次冲通道接收
   for i := 0; i < cap(done); i++ {
       <-done
  }
}

简单的做法实现同步--->使用等待组:

package main

import (
   "fmt"
   "sync"
)

/*
采用等待组的方法实现同步操作
*/
func main() {
   // 声明等待组
   var wg sync.WaitGroup

   // 开启N个打印线程
   for i := 0; i < 10; i++ {
       // 每开启一个添加一个计数器
       /*
       wg.Add(1)用于增加等待事件的个数
       必须确保在后台线程启动之前执行(如果放到后台线程之中执行则不能保证被正常执行到)
        */
       wg.Add(1)

       // 开启协程
       go func() {
           fmt.Println("打印中!")
           // 等待组处理
           wg.Done()
      }()
  }

   // 等待组结束
   wg.Wait()
}

 

posted @   俊king  阅读(140)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· AI编程工具终极对决:字节Trae VS Cursor,谁才是开发者新宠?
· 开源Multi-agent AI智能体框架aevatar.ai,欢迎大家贡献代码
· Manus重磅发布:全球首款通用AI代理技术深度解析与实战指南
· 被坑几百块钱后,我竟然真的恢复了删除的微信聊天记录!
点击右上角即可分享
微信分享提示