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

开发 后端
在当今这个多核时代,并发编程的意义不言而喻。使用 Go 开发并发程序,操作起来非常简单,语言级别提供关键字 go 用于启动协程,并且在同一台机器上可以启动成千上万个协程。

[[426818]]

优雅的并发编程范式,完善的并发支持,出色的并发性能是 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 intint { 
    if x < 2 { 
        return x 
    } 
    return fib(x-1) + fib(x-2) 

  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
  • 22.
  • 23.
  • 24.
  • 25.
  • 26.
  • 27.
  • 28.
  • 29.

从执行结果来看,成功计算出了斐波那契数列的值,说明程序在 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) 
    } 

  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.

有问题了,屏幕上什么都没有,为什么呢?

这就要看 Go 程序的执行机制了。当一个程序启动时,只有一个 goroutine 来调用 main 函数,称为主 goroutine。新的 goroutine 通过 go 关键词创建,然后并发执行。当 main 函数返回时,不会等待其他 goroutine 执行完,而是直接暴力结束所有 goroutine。

那有没有办法解决呢?当然是有的,请往下看。

channel

一般写多进程程序时,都会遇到一个问题:进程间通信。常见的通信方式有信号,共享内存等。goroutine 之间的通信机制是通道 channel。

使用 make 创建通道:

ch := make(chan int) // ch 的类型是 chan int 
  • 1.

通道支持三个主要操作:send,receive 和 close。

ch <- x // 发送 
x = <-ch // 接收 
<-ch // 接收,丢弃结果 
 
close(ch) // 关闭 
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.

无缓冲 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) 
    } 

  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.

可以正常输出结果。

主 goroutine 会阻塞,直到读取到通道中的值,程序继续执行,最后退出。

缓冲 channel

创建一个容量是 5 的缓冲通道:

ch := make(chan int, 5) 
  • 1.

缓冲通道的发送操作在通道尾部插入一个元素,接收操作从通道的头部移除一个元素。如果通道满了,发送会阻塞,直到另一个 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<- intin <-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) 
 

  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
  • 22.
  • 23.
  • 24.
  • 25.
  • 26.
  • 27.
  • 28.
  • 29.
  • 30.
  • 31.
  • 32.
  • 33.

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() 

  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
  • 22.
  • 23.
  • 24.
  • 25.
  • 26.
  • 27.
  • 28.
  • 29.
  • 30.
  • 31.
  • 32.
  • 33.
  • 34.
  • 35.
  • 36.
  • 37.
  • 38.
  • 39.
  • 40.
  • 41.

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() 

  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
  • 22.
  • 23.
  • 24.
  • 25.
  • 26.
  • 27.
  • 28.
  • 29.
  • 30.
  • 31.
  • 32.
  • 33.
  • 34.
  • 35.
  • 36.
  • 37.
  • 38.

总结

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

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

通道有三种:

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

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

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

 

责任编辑:姜华 来源: AlwaysBeta
相关推荐

2024-09-06 10:48:13

2024-10-14 08:51:52

协程Go语言

2024-06-19 10:08:34

GoChannel工具

2025-01-15 09:13:53

2022-03-04 10:07:45

Go语言字节池

2021-07-06 07:46:07

Go语言编程

2023-02-10 09:40:36

Go语言并发

2025-01-16 09:43:10

2021-06-29 23:40:19

Golang语言并发

2023-12-29 08:42:46

高并发Go语言

2024-12-13 16:28:43

2022-08-08 08:31:55

Go 语言闭包匿名函数

2022-08-08 06:50:06

Go语言闭包

2023-11-28 08:01:48

互斥锁共享资源

2023-12-25 09:58:25

sync包Go编程

2025-03-24 00:25:00

Go语言并发编程

2020-12-27 10:15:44

Go语言channel管道

2024-07-01 08:44:42

Go语言协程

2022-10-17 08:07:13

Go 语言并发编程

2024-09-02 09:00:59

点赞
收藏

51CTO技术栈公众号