这是我参与「第三届青训营 -后端场」笔记创作活动的的第1篇笔记。
可以关注一下我的博客 wuhlan3.gitee.io/ 呀!
一、概述
Go 是并发式语言,而不是并行式语言。
并发和并行是老生常谈的问题了,这里就简单抛出一下定义:
- 并行是指两个或者多个事件在同一时刻发生(是一种通过多处理器来提高效率的能力);
- 并发是指两个或多个事件在同一时间间隔发生(可在单处理上,也可在多处理器上)。
Go语言原生支持并发。Go 使用Go 协程(Goroutine) 和信道(Channel)来处理并发。Go没有把并行放在首要位置,因为并发程序可能是并行的,也可能不是。一个设计良好的并行程序在并行方面也会表现非常出色。
二、Go协程
1.Go 协程是什么?
Go 协程可以看作是轻量级线程。与线程相比,创建一个 Go 协程的成本很小。因此在 Go 应用中,常常会看到有数以千计的 Go 协程并发地运行。
2.Go协程与线程的对比
| 特点 | 线程 | 协程 |
|---|---|---|
| 拥有的资源 | 程序计数器、寄存器、栈和状态字 | 寄存器上下文和栈 |
| 内存消耗 | MB级别 | KB级别 |
| 线程/协程切换 | 需要从用户态切换到内核态,调用内核提供的syscall底层函数。(保存和设置程序计数器、少量寄存器和栈) | 在用户态进行简单的现场保护和恢复即可(保存和设置寄存器上下文和栈)。 |
3.启动一个Go协程
func Hello() {
fmt.Println("Hello")
}
func main() {
go Hello()
fmt.Println("world")
}
多运行几遍,我们会发现有的时候可以输出两句话,有的时候只能输出一句话。为什么呢?这是因为启动了一个协程之后,主协程不会等待子协程,而是继续往下执行。如果主协程终止了,整个程序就终止了,所以子协程就没办法输出了。所以我们可以稍微改进一下。
func Hello() {
fmt.Println("Hello")
}
func main() {
go Hello()
time.Sleep(time.Second)
fmt.Println("World")
}
但是问题还是没有彻底解决。这里又引出了新的问题:我们怎么知道需要等待多久呢?等待时间短了不能确保子协程完整运行,等待时间长了,影响协程效率。这里就引出了同步的概念,Go编程语言中可以使用WaitGroup来进行同步。
三、同步与互斥
1.WaitGroup
WaitGroup是一个结构体,它包含着一个计数器且初始化为0。基本的方法如下:
- Add(n int) 可以是计数器加上n,通常在启动n个go协程之前调用;
- Done()可以使计数器减一,通常在一个go协程运行结束后调用;
- Wait()方法会阻塞调用它的 Go 协程,直到计数器变为 0 后才会停止阻塞。
所以我们可以对之前的程序进行改写。只有当3个子协程都运行结束了,主协程才会继续执行。
func Hello(wg *sync.WaitGroup) {
defer wg.Done()
fmt.Println("Hello")
}
func main() {
var wg sync.WaitGroup
for i := 0; i < 3; i++ {
wg.Add(1)
go Hello(&wg)
}
wg.Wait()
fmt.Println("World")
}
2.无缓冲信道
除了WaitGroup之外,我们还可以使用Channel来进行同步。首先来了解一下channel的基本用法:
- 初始化
a `` := make(chan ``int``),表示定义了一个int类型的channel; a <- data写入信道adata := <- a读取信道 a
可以看到,信道的使用方法是非常形象的,它就像一个管道一样,箭头标明了数据的流动方向。
那么我们该如何改写上面的程序呢?具体实现如下:
3.缓冲信道
无缓冲信道在发送和接收的过程中都是阻塞的。就是说,当信道里有数据的时候,协程需要一直等待,直到没有数据才能够写信道;当信道里没有数据的时候,协程也需要一直等待,直到有数据了,才能读取数据。
我们也可以创建一种有缓冲的信道,只有缓冲满了,才会阻塞协程继续写入数据:
a := make(chan int, 3),创建了一个容量为3的信道。
可以参考训练营里一个简单的生产者消费者例子:
- 这里一个协程负责生产数字0~9;
- 一个协程负责计算平方,并发送给主协程
func CalSquare() {
src := make(chan int)
dest := make(chan int, 3) //考虑消费者的速度会稍微慢一些,所以使用带缓冲的队列,
// 避免消费速度慢而导致生产速度慢
//这里使用了匿名函数
go func() {
defer close(src)
for i := 0; i < 10; i++ {
src <- i
}
}()
go func() {
defer close(dest)
for i := range src {
dest <- i * i
}
}()
for i := range dest {
println(i)
}
}
4.互斥锁
Go中的互斥锁使用方式比较简单,就不再赘述了。
- 定义全局的变量
lock sync.Mutex - 加锁
lock.Lock() - 释放锁
lock.Unlock()
... 以上是青训营提到的比较基础的并行编程内容。后续还要探究select、工作池等内容。
参考资料
[1] 字节跳动青训营——Go语言上手 - 工程实践
[2] studygolang.com/