Go并发编程基础知识 | 青训营笔记

99 阅读4分钟

这是我参与「第五届青训营」伴学笔记创作活动的第9天。

更高效的并发编程是Go语言的重要特色之一,下面将会介绍一些Go语言并发编程的基础知识。

Go语言的并发编程主要依靠Goroutine(协程)、Channel(通道)这两种机制。此外,Go语言还有锁(Lock)机制和WaitGroup来进行并发通信。

这篇文章将会介绍Goroutine和Channel两个机制的基本概念和用法。

goroutine 协程

协程拥有独立的栈空间,共享堆空间,调度由用户自己控制,本质上有点类似于用户级线程,这些用户级线程的调度也是自己实现的。在一个线程上可以跑多个协程,协程是轻量级的线程。

具体来说,协程相较于线程来说,栈空间更小,一般只有4~5KB,而且因为调度均在用户态进行,省去了CPU在用户态和内核态转换的成本,因而相较于线程更加轻量和高效,很适合用来运行一些不那么重的任务。

需要注意的是,goroutine奉行通过通信来共享内存,而不是共享内存来通信。

goroutine的使用非常简单,只需要在调用函数(可以是普通函数,也可以是匿名函数)时在前面加上一个go关键字即可创建一个goroutine来执行这个函数,和线程类似,可以创建多个goroutine执行同一个函数。需要注意的是,goroutine的执行不会阻塞程序的运行,比如:

func hello() {
    fmt.Println("Hello Goroutine!")
}
func main() {
    go hello() // 启动另外一个goroutine去执行hello函数
    fmt.Println("main goroutine done!")
}

这段代码可能只会打印出main goroutine done!而不会打印出Hello Goroutine!。这是因为goroutine的执行不会阻塞程序的运行,因此,在执行go hello()之后,程序不会等待hello函数完成运行,而是直接向下继续运行,打印main goroutine done!,并在之后结束主协程的运行,其他所有协程都会和主协程一起结束。如果此时hello函数还没有完成打印,就不会有机会去执行打印语句了。

前面提到goroutine的栈空间内存占用(一般在4~5KB左右)比线程(一般在2MB左右)要小很多,这是因为内存分配的策略不同导致的,系统线程空间分配通常是固定大小的,而goroutine则比较灵活,栈空间初始情况下会分配一个较小的空间(典型情况为2KB),之后随着需要逐渐增长,最大一般能够达到1GB。由于大多数由协程执行的任务用不到太大的栈空间,所以goroutine的内存分配策略减少了栈空间的浪费,可以支撑大量(比如十万)goroutine运行,在高并发环境更具优势。

channel 通道

channel是Go语言的goroutine并发通信的一个重要机制,Go语言的并发模型是CSP(Communicating Sequential Processes),相较于通过共享内存来进行通信更推崇使用channel等通信机制来进行通信。

Go语言中的通道(channel)是一种特殊的类型。通道像一个传送带或者队列,总是遵循先入先出(First In First Out)的规则,保证收发数据的顺序。每一个通道都是一个具体类型的导管,也就是声明channel的时候需要为其指定元素类型。

channel需要通过make方法来创建,形如make(chan 元素类型, [缓冲大小]),其中,缓冲大小是可选的,默认为0(无缓冲,需要数据被接收后才能继续发送,在数据被接收前会阻塞发送操作)如下所示:

ch4 := make(chan int)
ch5 := make(chan bool)
ch6 := make(chan []int)

channel有发送(send)、接收(receive)和关闭(close)三种操作,发送和接收都使用<-符号。下面是一些例子:

ch <- 10 // 把10发送到ch中

x := <- ch // 从ch中接收值并赋值给变量x
<-ch       // 从ch中接收值,忽略结果

close(ch) // 关闭ch

另外,与文件打开后必须有关闭操作不同,关闭channel这一操作并不是必须进行的。channel可以被垃圾回收机制自动回收,所以只需要在发送方需要告知接收方数据发送完毕时需要手动关闭channel。