240830_Go并发编程:Goroutines和Channels

Haoliang Tang Lv3

The great thing about Go is how easy it makes it to write concurrent code.

Go对并发编程的支持也是Go的一个亮点,Go中采用的并发编程模型是CSP,这区别于其他很多的编程语言基于传统的多线程共享内存而实现的并发特性。(当然Go也可以通过共享内存来做到并发)

和传统的加锁方式不同,CSP理论中不允许进程对其他进程的变量赋值,进程之间只能通过通信原语来实现数据交换和协作。但是其他语言都没能真正地为这些原语提供支持。大多数流行语言都支持共享和内存访问同步到CSP的消息传递样式。而采用CSP理论的语言也有,但是基本都没有得到广泛的采用。

直到Go语言出现,正式将CSP的原则纳入到核心原则,并提供了通信的原语支持channel。正因为如此,并发被认为是Go语言的优势之一。并让Go语言在分布式系统、云原生系统和系统中间件中迅速占领了市场。而其他语言基于共享内存的并发模型,在大型和复杂系统中变得很难正确使用。

Go同时也支持传统的加锁方式,也支持共享内存的方式。不过Go已经向其他语言证明CSP的可行性和优越处。并且提出了经典的CSP核心概念:

1
Do not communicate by sharing memory; instead, share memory by communicating.“不要以共享内存的方式来通信,相反,要通过通信来共享内存。”

“不要通过共享内存来进行通信,而是通过通信来共享内存”。

在计算机科学中,有时候不同的进程或线程需要相互传递数据,这个过程叫做通信。传递数据的方式有很多种,其中一种是共享内存。共享内存意味着多个进程或线程可以访问同一块内存空间。但是,共享内存可能会导致并发问题,例如竞争条件(Race condition),死锁(Deadlocks)等等。

所以,这句话告诉我们,要想避免这些问题,最好的方式是通过通信来共享内存。也就是说,进程或线程之间应该使用某些机制进行数据传递,例如消息队列、Socket等方法。不同进程或线程之间通过这些通信机制共享数据,以避免共享内存造成的并发问题。

八股: 并发与并行 Concurrency / Parallelism

https://xiaolincoding.com/os/4_process/process_base.html#%E8%BF%9B%E7%A8%8B

https://developer.baidu.com/article/details/3272650

并发(Concurrent 日语:並行): 一个处理器上通过快速切换任务,交替执行来模拟同时执行多个任务的效果

并行(Parallel 日语:並列): 多个处理器上真正的同时执行多个任务

Goroutine

可以简单认为goroutine就是一个轻量线程, A goroutine is a lightweight thread of execution.(协程coroutine)

当一个程序启动时,其主函数即在一个单独的goroutine中运行,我们叫它main goroutine。新的goroutine会用go语句来创建。在语法上,go语句是一个普通的函数或方法调用前加上关键字go。go语句会使其语句中的函数在一个新创建的goroutine中运行。

1
2
f()    // call f(); wait for it to return
go f() // create a new goroutine that calls f(); don't wait

你就当要执行的函数加了个go,就另外开了一个线程并行地在CPU的另一个core上执行了

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
package main

import (
"fmt"
"time"
)

func main() {
go spinner(100 * time.Millisecond) // 没有go的话,就直接死循环了
const n = 45
fibN := fib(n) // slow
fmt.Printf("\rFibonacci(%d) = %d\n", n, fibN)
}

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

主函数返回时,所有的goroutine都会被直接打断(goroutine没有実行完にもかかわらず),程序退出。(所以明明死循环的spinner函数随着main协程的结束也终止了)

When we use the go keyword, we are not able to capture any return values from the function call. 就仅仅当成在另一个thread上做它的事情

WaitGroup

主函数返回时,所有的goroutine都会被直接打断(goroutine没有実行完にもかかわらず)

那怎么才能让main函数去等goroutine执行完呢?

To wait for multiple goroutines to finish, we can use a wait group.

为了知道最后一个goroutine什么时候结束(最后一个结束并不一定是最后一个开始),我们需要一个递增的计数器,在每一个goroutine启动时加一wg.Add(1),在goroutine退出时减一defer wg.Done()。这需要一种特殊的计数器,这个计数器需要在多个goroutine操作时做到安全并且提供在其减为零之前一直等待的一种方法wg.Wait()。这种计数类型被称为sync.WaitGroup

Channel

既然没法从其他goroutine中获得函数返回值,那如何获取其他goroutine的计算结果呢?

channels就是用来做goroutine间的通信的。(再次印证了各个goroutine是相互独立的,通过通信来共享内存)

每个channel都有一个类型,也就是channels可发送数据的类型。可以把buffered channel看作只能收发某种特定数据类型的消息队列(底层数据结构是环状的循环链表)。

使用内置的make函数,我们可以创建一个channel:

1
ch := make(chan int) // ch has type 'chan int'

以最简单方式调用make函数创建的是一个无缓存的channel,但是我们也可以指定第二个整型参数,对应channel的容量。如果channel的容量大于零,那么该channel就是带缓存的channel。

1
2
3
ch = make(chan int)    // unbuffered channel
ch = make(chan int, 0) // unbuffered channel
ch = make(chan int, 3) // buffered channel with capacity 3

超过channel的容量了还继续入队会触发阻塞

channel有发送和接受两个主要操作,都是通信行为

1
2
3
ch <- x  // a send statement
x = <-ch // a receive expression in an assignment statement
<-ch // a receive statement; result is discarded

Channel还支持close操作,用于关闭channel,随后对基于该channel的任何发送操作都将导致panic异常。对一个已经被close过的channel进行接收操作依然可以接受到之前已经成功发送的数据;如果channel中已经没有数据的话将产生一个零值的数据。

1
close(ch)

unbuffered channel可以用来同步两个goroutine,buffer容量为1可以用来当作一个锁,二元信号量(binary semaphore)

参考资料

Learning Go书Ch12

https://gopl-zh.github.io/ch8/ch8.html Go语言圣经

https://www.youtube.com/watch?v=un6ZyFkqFKo Ch13.Channels&concurrency Ch14.Mutexes

用多线程给代码提速800% —— Golang高并发教程+实战 TheCW

Master Go Programming With These Concurrency Patterns (in 40 minutes)

  • Title: 240830_Go并发编程:Goroutines和Channels
  • Author: Haoliang Tang
  • Created at : 2024-08-30 00:00:00
  • Updated at : 2025-04-30 00:03:36
  • Link: https://hl-tang.github.io/2024/08/30/240830_Go并发编程:Goroutines和Channels/
  • License: This work is licensed under CC BY-NC-SA 4.0.
Comments
On this page
240830_Go并发编程:Goroutines和Channels