这是我参与【第五届青训营】伴学笔记创作活动的第2天
Go工程实践
Goroutine(轻量级线程)
并发和并行
Go语言是为并发而生的语言,Go语言是为数不多的在语言层面实现并发的语言。
并发(concurrency):多个任务在同一段时间内运行。
并行(parallellism):多个任务在同一时刻运行。
Go实现了两种并发形式。
- 多线程共享内存:Java或者C++等语言中的多线程开发。
- CSP(communicating sequential processes)并发模型:Go语言特有且推荐使用的。
CSP讲究的是“以通信的方式来共享内存”。go也保存着通过共享内存实现通信的机制(通过互斥量对内存进行加锁),但是提倡通过通信共享内存
-
普通的线程并发模型,就是像Java、C++、或者Python,他们线程间通信都是通过共享内存的方式来进行的。在访问共享数据(例如数组、Map、或者某个结构体或对象)的时候,通过锁来访问,因此,衍生出一种方便操作的数据结构,叫做“线程安全的数据结构”。
-
Go的CSP并发模型,是通过goroutine和channel来实现的。
- goroutine 是Go语言中并发的执行单位。可以理解为用户空间的线程。
- channel是Go语言中不同goroutine之间的通信机制,即各个goroutine之间通信的”管道“,有点类似于Linux中的管道。
goroutine介绍
- 协程:用户态,轻量级线程,栈KB级别
- 线程:内核态,线程跑多个协程,栈MB级别
Go 程序中使用 go 关键字为一个函数创建一个 goroutine。一个函数可以被创建多个 goroutine,一个 goroutine 必定对应一个函数。所有 goroutine 在 main() 函数结束时会一同结束。
goroutine的优点:
-
创建与销毁的开销小
- 线程创建时需要向操作系统申请资源,并且在销毁时将资源归还,因此它的创建和销毁的开销比较大。相比之下,goroutine的创建和销毁是由go语言在运行时自己管理的,因此开销更低。所以一个Golang的程序中可以支持10w级别的Goroutine。每个 goroutine (协程) 默认占用内存远比 Java 、C 的线程少(goroutine: 2KB ,线程:8MB)
-
切换开销小
- 这是goroutine于线程的主要区别,也是golang能够实现高并发的主要原因。
- 线程的调度方式是抢占式的,如果一个线程的执行时间超过了分配给它的时间片,就会被其它可执行的线程抢占。在线程切换的过程中需要保存/恢复所有的寄存器信息。
- goroutine的调度是协同式的,没有时间片的概念,由Golang完成,它不会直接地与操作系统内核打交道。当goroutine进行切换的时候,之后很少量的寄存器需要保存和恢复(PC和SP)。因此gouroutine的切换效率更高。
goroutine 虽然类似于线程概念,但是从调度性能上没有线程细致,而细致程度取决于 Go 程序的 goroutine 调度器的实现和运行环境。
使用普通函数创建goroutine
go 函数名( 参数列表 )
package concurrence
import (
"fmt"
"time"
)
func hello(i int) {
println("hello goroutine : " + fmt.Sprint(i))
}
func HelloGoRoutine() {
for i := 0; i < 5; i++ {
go func(j int) { // 并发执行程序
hello(j)
}(i)
}
time.Sleep(time.Second)
}
使用匿名函数创建goroutine
go func( 参数列表 ){
函数体
}( 调用参数列表 )
- 参数列表:函数体内的参数变量列表。
- 函数体:匿名函数的代码。
- 调用参数列表:启动 goroutine 时,需要向匿名函数传递的调用参数。
package concurrence
import (
"fmt"
"time"
)
func hello(i int) {
println("hello goroutine : " + fmt.Sprint(i))
}
func HelloGoRoutine() {
for i := 0; i < 5; i++ {
go func(j int) {
hello(j)
}(i)
}
time.Sleep(time.Second)
}
如果需要在 goroutine 中返回数据,使用后面介绍的通道(channel)特性,通过通道把数据从 goroutine 中作为返回值传出。
channel
介绍
- channel本身是一个队列,先进先出
- 线程安全,不需要加锁
- 本身是有类型的,string, int 等,如果要存多种类型,则定义成 interface类型
- channel是引用类型,必须make之后才能使用,一旦 make,它的容量就确定了,不会动态增加!!它和map,slice不一样
定义及使用
make(chan 元素类型, [缓冲大小])
- 无缓冲通道
make(chan int),只有通道中的元素被消费了,才能继续推送,不然就会阻塞 - 有缓冲通道
make(chan int, 2)
如果没有指定方向,那么Channel就是双向的,既可以接收数据,也可以发送数据。
它的操作符是箭头 <- 。
ch <- v // 发送值v到Channel ch中
通道的数据接收
-
阻塞接收数据
阻塞模式接收数据时,将接收变量作为<-操作符的左值,格式如下:
//执行该语句时将会阻塞,直到接收到数据并赋值给 data 变量。
data := <-ch // 从Channel ch中接收数据,并将数据赋值给data
-
非阻塞接收数据
使用非阻塞方式从通道接收数据时,语句不会发生阻塞,格式如下:
data, ok := <-ch //有问题时,还是会报错deadlock
-
接收任意数据,忽略接收的数据
阻塞接收数据后,忽略从通道返回的数据,比如我们希望获得到管道中的第二个元素,则先将第一个元素推出,格式如下:
<-ch
执行该语句时将会发生阻塞,直到接收到数据,但接收到的数据会被忽略。这个方式实际上只是通过通道在 goroutine 间阻塞收发实现并发同步。
-
循环接收
通道的数据接收可以借用 for range 语句进行多个元素的接收操作
for data := range ch {
}
通道 ch 是可以进行遍历的,遍历的结果就是接收到的数据。数据类型就是通道的数据类型。通过 for 遍历获得的变量只有一个,即上面例子中的 data。在遍历管道之前要先关闭管道,不然会出现deadlock的错误
package concurrence
func CalSquare() {
src := make(chan int)
dest := make(chan int, 3)
// 生产者
go func() {
defer close(src) //在遍历管道之前要先关闭管道
for i := 0; i < 10; i++ {
src <- i //向管道src写入数据
}
}()
// 消费者 -> 生产者
go func() {
defer close(dest)
for i := range src {
dest <- i * i
}
}()
for i := range dest {
//复杂操作
println(i)
}
}
关闭通道
close(chan)
Lock
当多个 goroutine 同时操作一个变量时,会存在数据竞争,导致最后的结果与期待的不符,解决办法就是加锁。
Go 中的 sync 包 实现了两种锁:Mutex 和 RWMutex,前者为互斥锁,后者为读写锁,基于 Mutex 实现。当我们的场景是写操作为主时,可以使用 Mutex 来加锁、解锁。
package main
import (
"sync"
"time"
)
var (
x int64
lock sync.Mutex
)
func addWithLock() {
for i := 0; i < 2000; i++ {
lock.Lock()
x += 1
lock.Unlock()
}
}
func addWithoutLock() {
for i := 0; i < 2000; i++ {
x += 1
}
}
func main() {
x = 0
for i := 0; i < 5; i++ {
go addWithoutLock()
}
time.Sleep(time.Second)
println("WithoutLock:", x)
x = 0
for i := 0; i < 5; i++ {
go addWithLock()
}
time.Sleep(time.Second)
println("WithLock:", x)
}
对遍历执行2000次+1操作,5个协程并发执行加锁与不加锁的执行结果
WaitGroup
WatiGroup 能够一直等到所有的 goroutine 执行完成,并且阻塞主线程的执行,直到所有的 goroutine 执行完成。它有 3 个方法:
- Add():给计数器添加等待 goroutine 的数量。
- Done():计数器-1
- Wait():阻塞直到计数器为0
package concurrence
import (
"sync"
"time"
)
func hello(i int) {
println("hello goroutine : " + fmt.Sprint(i))
}
func ManyGoWait() {
var wg sync.WaitGroup
wg.Add(5)
for i := 0; i < 5; i++ {
go func(j int) {
defer wg.Done()
hello(j)
}(i)
}
wg.Wait()
}
总结:
Go语言中goroutine channel是区别与其他语言的一大特点,正是因为对goroutine和channel的使用,可以避免我们用互斥锁来让它们相互协作,使用起来更加便捷。