Go 专栏|并发编程:goroutine,channel 和 sync

简介: Go 专栏|并发编程:goroutine,channel 和 sync

QQ图片20220423133827.png

原文链接:Go 专栏|并发编程:goroutine,channel 和 sync


优雅的并发编程范式,完善的并发支持,出色的并发性能是 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
复制代码


通道支持三个主要操作:sendreceiveclose


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.Mutexsync.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()
}
复制代码


RWMutex 属于经典的单写多读模型,当读锁被占用时,会阻止写,但不阻止读。而写锁会阻止写和读。


package main
import (
  "fmt"
  "sync"
  "time"
)
func main() {
  var rwMutex sync.RWMutex
  wg := sync.WaitGroup{}
  Data := 0
  wg.Add(20)
  for i := 0; i < 10; i++ {
    go func(t int) {
      // 第一次运行后,写解锁。
      // 循环到第二次时,读锁定后,goroutine 没有阻塞,同时读成功。
      fmt.Println("Locking")
      rwMutex.RLock()
      defer rwMutex.RUnlock()
      fmt.Printf("Read data: %v\n", Data)
      wg.Done()
      time.Sleep(2 * time.Second)
    }(i)
    go func(t int) {
      // 写锁定下是需要解锁后才能写的
      rwMutex.Lock()
      defer rwMutex.Unlock()
      Data += t
      fmt.Printf("Write Data: %v %d \n", Data, t)
      wg.Done()
      time.Sleep(2 * time.Second)
    }(i)
  }
  wg.Wait()
}
复制代码


总结


并发编程算是 Go 的特色,也是核心功能之一了,涉及的知识点其实是非常多的,本文也只是起到一个抛砖引玉的作用而已。


本文开始介绍了 goroutine 的简单用法,然后引出了通道的概念。


通道有三种:


  1. 无缓冲通道
  2. 缓冲通道
  3. 单向通道


最后介绍了 Go 中的锁机制,分别是 sync 包提供的 sync.Mutex(互斥锁) 和 sync.RWMutex(读写锁)。


goroutine 博大精深,后面的坑还是要慢慢踩的。




文章中的脑图和源码都上传到了 GitHub,有需要的同学可自行下载。


地址:github.com/yongxinz/go…



目录
相关文章
|
1月前
|
并行计算 安全 Go
Go语言中的并发编程:掌握goroutines和channels####
本文深入探讨了Go语言中并发编程的核心概念——goroutine和channel。不同于传统的线程模型,Go通过轻量级的goroutine和通信机制channel,实现了高效的并发处理。我们将从基础概念开始,逐步深入到实际应用案例,揭示如何在Go语言中优雅地实现并发控制和数据同步。 ####
|
2月前
|
安全 Serverless Go
Go语言中的并发编程:深入理解与实践####
本文旨在为读者提供一个关于Go语言并发编程的全面指南。我们将从并发的基本概念讲起,逐步深入到Go语言特有的goroutine和channel机制,探讨它们如何简化多线程编程的复杂性。通过实例演示和代码分析,本文将揭示Go语言在处理并发任务时的优势,以及如何在实际项目中高效利用这些特性来提升性能和响应速度。无论你是Go语言的初学者还是有一定经验的开发者,本文都将为你提供有价值的见解和实用的技巧。 ####
|
2月前
|
Go 调度 开发者
Go语言中的并发编程:深入理解goroutines和channels####
本文旨在探讨Go语言中并发编程的核心概念——goroutines和channels。通过分析它们的工作原理、使用场景以及最佳实践,帮助开发者更好地理解和运用这两种强大的工具来构建高效、可扩展的应用程序。文章还将涵盖一些常见的陷阱和解决方案,以确保在实际应用中能够避免潜在的问题。 ####
|
2月前
|
安全 Go 数据处理
Go语言中的并发编程:掌握goroutine和channel的艺术####
本文深入探讨了Go语言在并发编程领域的核心概念——goroutine与channel。不同于传统的单线程执行模式,Go通过轻量级的goroutine实现了高效的并发处理,而channel作为goroutines之间通信的桥梁,确保了数据传递的安全性与高效性。文章首先简述了goroutine的基本特性及其创建方法,随后详细解析了channel的类型、操作以及它们如何协同工作以构建健壮的并发应用。此外,还介绍了select语句在多路复用中的应用,以及如何利用WaitGroup等待一组goroutine完成。最后,通过一个实际案例展示了如何在Go中设计并实现一个简单的并发程序,旨在帮助读者理解并掌
|
2月前
|
算法 安全 程序员
Go语言的并发编程:深入理解与实践####
本文旨在探讨Go语言在并发编程方面的独特优势及其实现机制,通过实例解析关键概念如goroutine和channel,帮助开发者更高效地利用Go进行高性能软件开发。不同于传统的摘要概述,本文将以一个简短的故事开头,引出并发编程的重要性,随后详细阐述Go语言如何简化复杂并发任务的处理,最后通过实际案例展示其强大功能。 --- ###
|
存储 缓存 Java
浅析Go中Channel的各路用法
浅析Go中Channel的各路用法
31581 0
浅析Go中Channel的各路用法
|
Go
golang channel 用法转的
一、Golang并发基础理论 Golang在并发设计方面参考了C.A.R Hoare的CSP,即Communicating Sequential Processes并发模型理论。但就像John Graham-Cumming所说的那样,多数Golang程序员或爱好者仅仅停留在“知道”这一层次,理解CSP理论的并不多,毕竟多数程序员是搞工程 的。
1196 0
|
15天前
|
存储 监控 算法
员工上网行为监控中的Go语言算法:布隆过滤器的应用
在信息化高速发展的时代,企业上网行为监管至关重要。布隆过滤器作为一种高效、节省空间的概率性数据结构,适用于大规模URL查询与匹配,是实现精准上网行为管理的理想选择。本文探讨了布隆过滤器的原理及其优缺点,并展示了如何使用Go语言实现该算法,以提升企业网络管理效率和安全性。尽管存在误报等局限性,但合理配置下,布隆过滤器为企业提供了经济有效的解决方案。
58 8
员工上网行为监控中的Go语言算法:布隆过滤器的应用
|
1月前
|
存储 Go 索引
go语言中数组和切片
go语言中数组和切片
42 7