加入收藏 | 设为首页 | 会员中心 | 我要投稿 财气旺网 - 财气网 (https://www.caiqiwang.com/)- 科技、建站、经验、云计算、5G、大数据,站长网!
当前位置: 首页 > 综合聚焦 > 编程要点 > 语言 > 正文

Go语言里的并发 编程 Goroutine,Channel和Sync

发布时间:2022-11-26 12:38:07 所属栏目:语言 来源:
导读:  优雅的并发编程范式,完善的并发支持,出色的并发性能是 Go 语言区别于其他语言的一大特色。
  
  在当今这个多核时代,并发编程的意义不言而喻。使用 Go 开发并发程序,操作起来非常简单,语言级别提供

  优雅的并发编程范式,完善的并发支持,出色的并发性能是 Go 语言区别于其他语言的一大特色。
  
  在当今这个多核时代,并发编程的意义不言而喻。使用 Go 开发并发程序,操作起来非常简单,语言级别提供关键字 go 用于启动协程,并且在同一台机器上可以启动成千上万个协程。
  
  下面就来详细介绍。
  
  goroutine
  Go 语言的并发执行体称为 goroutine,使用关键词 go 来启动一个 goroutine。
  
  go 关键词后面必须跟一个函数,可以是有名函数,也可以是无名函数,函数的返回值会被忽略。
  
  go 的执行是非阻塞的。
  
  先来看一个例子:
  
  package main
  
  import (
      "fmt"
      "time"
  )
  
  func main() {
      go spinner(100 * time.Millisecond)
      const n = 45
      fibN := fib(n)
      fmt.Printf("\rFibonacci(%d) = %d\n", n, fibN) // Fibonacci(45) = 1134903170
  }
  
  func spinner(delay time.Duration) {
      for {
          for _, r := range `-\|/` {
              fmt.Printf("\r%c", r)
              time.Sleep(delay)
          }
      }
  }
  
  func fib(x int) int {
      if x < 2 {
          return x
      }
      return fib(x-1) + fib(x-2)
  }
  从执行结果来看,成功计算出了斐波那契数列的值,说明程序在 spinner 处并没有阻塞,而且 spinner 函数还一直在屏幕上打印提示字符,说明程序正在执行。
  
  当计算完斐波那契数列的值,main 函数打印结果并退出,spinner 也跟着退出。
  
  再来看一个例子,循环执行 10 次,打印两个数的和:
  
  package main
  
  import "fmt"
  
  func Add(x, y int) {
      z := x + y
      fmt.Println(z)
  }
  
  func main() {
      for i := 0; i < 10; i++ {
          go Add(i, i)
      }
  }
  有问题了,屏幕上什么都没有,为什么呢?
  
  这就要看 Go 程序的执行机制了。当一个程序启动时,只有一个 goroutine 来调用 main 函数,称为主 goroutine。新的 goroutine 通过 go 关键词创建,然后并发执行。当 main 函数返回时,不会等待其他 goroutine 执行完,而是直接暴力结束所有 goroutine。
  
  那有没有办法解决呢?当然是有的,请往下看。
  
  channel
  一般写多进程程序时,都会遇到一个问题:进程间通信。常见的通信方式有信号,共享内存等。goroutine 之间的通信机制是通道 channel。
  
  使用 make 创建通道:
  
  ch := make(chan int) // ch 的类型是 chan int
  通道支持三个主要操作:send,receive 和 close。
  
  ch <- x // 发送
  x = <-ch // 接收
  <-ch // 接收,丢弃结果
  
  close(ch) // 关闭
  无缓冲 channel
  make 函数接受两个参数,第二个参数是可选参数,表示通道容量。不传或者传 0 表示创建了一个无缓冲通道。
  
  无缓冲通道上的发送操作将会阻塞,直到另一个 goroutine 在对应的通道上执行接收操作。相反,如果接收先执行,那么接收 goroutine 将会阻塞,直到另一个 goroutine 在对应通道上执行发送。
  
  所以,无缓冲通道是一种同步通道。
  
  下面我们使用无缓冲通道把上面例子中出现的问题解决一下。
  
  package main
  
  import "fmt"
  
  func Add(x, y int, ch chan int) {
      z := x + y
      ch <- z
  }
  
  func main() {
  
      ch := make(chan int)
      for i := 0; i < 10; i++ {
          go Add(i, i, ch)
      }
  
      for i := 0; i < 10; i++ {
          fmt.Println(<-ch)
      }
  }
  可以正常输出结果。
  
  主 goroutine 会阻塞,直到读取到通道中的值,程序继续执行,最后退出。
  
  缓冲 channel
  创建一个容量是 5 的缓冲通道:
  
  ch := make(chan int, 5)
  缓冲通道的发送操作在通道尾部插入一个元素,接收操作从通道的头部移除一个元素。如果通道满了,发送会阻塞,直到另一个 goroutine 执行接收。相反,如果通道是空的,接收会阻塞,直到另一个 goroutine 执行发送。
  
  有没有感觉,其实缓冲通道和队列一样,把操作都解耦了。
  
  单向 channel
  类型 chan<- int 是一个只能发送的通道,类型 <-chan int 是一个只能接收的通道。
  
  任何双向通道都可以用作单向通道,但反过来不行。
  
  还有一点需要注意,close 只能用在发送通道上,如果用在接收通道会报错。
  
  看一个单向通道的例子:
  
  package main
  
  import "fmt"
  
  func counter(out chan<- int) {
      for x := 0; x < 10; x++ {
          out <- x
      }
      close(out)
  }
  
  func squarer(out chan<- int, in <-chan int) {
      for v := range in {
          out <- v * v
      }
      close(out)
  }
  
  func printer(in <-chan int) {
      for v := range in {
          fmt.Println(v)
      }
  }
  
  func main() {
      n := make(chan int)
      s := make(chan int)
  
      go counter(n)
      go squarer(s, n)
      printer(s)
  
  }
  sync
  sync 包提供了两种锁类型:sync.Mutex 和 sync.RWMutex,前者是互斥锁,后者是读写锁。
  
  当一个 goroutine 获取了 Mutex 后,其他 goroutine 不管读写,只能等待,直到锁被释放。
  
  package main
  
  import (
      "fmt"
      "sync"
      "time"
  )
  
  func main() {
      var mutex sync.Mutex
      wg := sync.WaitGroup{}
  
      // 主 goroutine 先获取锁
      fmt.Println("Locking  (G0)")
      mutex.Lock()
      fmt.Println("locked (G0)")
  
      wg.Add(3)
      for i := 1; i < 4; i++ {
          go func(i int) {
              // 由于主 goroutine 先获取锁,程序开始 5 秒会阻塞在这里
              fmt.Printf("Locking (G%d)\n", i)
              mutex.Lock()
              fmt.Printf("locked (G%d)\n", i)
  
              time.Sleep(time.Second * 2)
              mutex.Unlock()
              fmt.Printf("unlocked (G%d)\n", i)
  
              wg.Done()
          }(i)
      }
  
      // 主 goroutine 5 秒后释放锁
      time.Sleep(time.Second * 5)
      fmt.Println("ready unlock (G0)")
      mutex.Unlock()
      fmt.Println("unlocked (G0)")
  
      wg.Wait()
  }
 

(编辑:财气旺网 - 财气网)

【声明】本站内容均来自网络,其相关言论仅代表作者个人观点,不代表本站立场。若无意侵犯到您的权利,请及时与联系站长删除相关内容!