这是我参与「第三届青训营 -后端场」笔记创作活动的的第2篇笔记
本文主要记录 goroutine 并发模式
程序通常是顺序执行并完成一个独立的任务,不过也有很多需求是需要并行执行多个任务,这样可以极大地减少任务执行总时间。比如 Web 中,需要在各自独立的接口上同时处理多个数据请求。
Go 语言中的并发是指某个函数能独立于其他函数运行。同步模型来自 CSP(通信顺序进程)。它是一种消息传递模型,通过在 goroutine 之间传递数据来传递消息,而不是通过对数据加锁来实现同步访问。在 goroutine 之间同步和传递数据使用的数据结构叫通道(channel)。
并发和并行
当一个应用程序运行时,系统会为这个程序开启一个进程,并且分配在运行中可能使用到的各种资源。 每个进程至少包括一个线程,进程的初始线程被称为主线程,当主线程停止后,整个应用程序也会关闭。
并行不是并发。并行指任务在不同的处理器单元上同时执行。而并发是逻辑上同时运行,一个任务可以被暂停,然后处理器资源可以去处理其他任务,这样就达到了同时运行多个任务的目的。
goroutine
package main
import (
"fmt"
"time"
)
func printNum(num int) {
for i := 0; i < 3; i++ {
fmt.Println(num, i)
}
}
func main() {
go printNum(1)
go printNum(2)
time.Sleep(time.Duration(time.Second))
}
输出结果
1 0
2 0
2 1
2 2
1 1
1 2
如果不加 time.Sleep(time.Duration(time.Second)) 这行代码,没有输出结果,因为 printNum 还没有执行完毕,主线程就结束了。
这里声明了一个 printNum 函数进行数据打印,num 为 goroutine 编号。使用 go 关键字来创建 goroutine。可以看出结果并不是按照顺序执行的输出,因为这两个 goroutine 是并发执行的。
还可以使用 sync.WaitGroup 来监控 goroutine 是否完成。
互斥锁
package main
import (
"fmt"
"sync"
)
var count int
var wg sync.WaitGroup
func printNum(num int) {
defer wg.Done()
for i := 0; i < 10000; i++ {
count++
}
}
func main() {
wg.Add(2)
go printNum(1)
go printNum(2)
wg.Wait()
fmt.Println(count)
}
上述代码输出 12301(每次均不同),原因是 count 临界资源,两个 goroutine 竞争的时候发生了读写问题。
可以在临界区加入互斥锁(mutux)来保证正确的输出。
var mutex sync.Mutex
func printNum(num int) {
mutex.Lock()
defer wg.Done()
for i := 0; i < 10000; i++ {
count++
}
mutex.Unlock()
}
如上所示,使用 mutex.Lock() 和 mutex.Unlock() 来锁住临界代码。
通道
使用通道,通过发送和接受需要共享的资源,当一个资源需要在 goroutine 之间共享是,通道就是其桥梁。
c := make(chan int) // 无缓冲的通道
c3 := make(chan int, 3) // 有缓冲的通道
c <- 1 // 写入通道
v := <- c // 读取通道内容
下面是一个简单同步示例来保证资源安全访问
package main
import (
"fmt"
"time"
)
func main() {
c := make(chan int)
go func() {
time.Sleep(time.Duration(time.Second * 3))
c <- 1
}()
fmt.Println(<-c)
}
输出结果为 1,可以发现 main 函数中并未使用时间延时来保持主线程,但是还是得到了正确的输出结果。
原因是当程序运行到 fmt.Println(<-c) 后,读取通道会被锁住,等待通道 c 中被写入数据。当执行到 c <- 1,写入通道代码也会锁住,等数据交换完成后,代码立即执行。
func main() {
c := make(chan int)
go func() {
time.Sleep(time.Duration(time.Second * 3))
fmt.Println(<-c)
}()
c <- 1
}
// 输出 1
func main() {
c := make(chan int, 1)
go func() {
time.Sleep(time.Duration(time.Second * 3))
fmt.Println(<-c)
}()
c <- 1
}
// 无任何输出
若是仅写入或者仅读取均会错误,因为已经造成了死锁。