这是我参与「第五届青训营」伴学笔记创作活动的第 2 天
协程
协程是轻量级的线程。Go语言很好地实现了协程,并且优化做得很好,一个线程上能轻松开启几万条协程。 Go协程的特点:
- 有独立的栈空间
- 共享程序堆空间
- 调度由用户控制
- 协程是轻量级线程
进程、线程的调度都是由操作系统控制的,而协程则是完全由用户控制的,操作系统感知不到协程的存在。
快速入门
使用go
关键字开启一个协程,协程以函数为单位。
- 在主线程中,开启一个协程,每隔 1 秒输出 “hello world”,输出 30 次
- 在主线程中也每隔 1 秒输出“hello golang”,输出 10 次后,退出程序
package main
import (
"fmt"
"strconv"
"time"
)
// 每隔一秒输出 “hello world”,输出 30 次
func test() {
for i := 0; i < 30; i++ {
fmt.Println("test() ===> hello world! " + strconv.Itoa(i))
// 间隔 1 秒
time.Sleep(time.Second)
}
}
func main() {
//test() // 顺序执行
go test() // 开启协程
for i := 0; i < 10; i++ {
fmt.Println("main() ===> hello go! " + strconv.Itoa(i))
// 间隔 1 秒
time.Sleep(time.Second)
}
}
执行流程:
- 线程是一个物理线程,直接作用在 CPU 上,是重量级的,非常耗费 CPU 资源。
- 协程由线程开启,是轻量级线程,是逻辑态,对资源消耗相对小。
- Golang的协程机制是重要的特点,可以轻松开启上万个协程。其他编程语言的并发机制一般都是基于线程的,开启过多的线程,资源耗费大。由此体现 Go 语言在并发上的优势。
协程通信
Go 提倡通过通信共享内存而不是通过共享内存而实现通信。
Channel
引用类型,通过make
关键字创建:
make(chan 通道中元素的数据类型, [缓冲大小])
- 如果不指定缓冲大小,则为无缓冲通道。无缓冲通道会使得发送的协程和接受的协程同步化。
- 如果指定缓冲大小,则为有缓冲通道。当缓冲区满的时候会阻塞发送,即生产者消费者模式。
- 带缓冲的 Channel 可以解决生产和消费速度不一致的问题。
实现生产者消费者模型:
func CalSquare() {
src := make(chan int)
dest := make(chan int, 3)
// A 协程(匿名函数)
go func() {
defer close(src) // 延迟调用,在函数最后执行关闭释放资源
for i := 0; i < 10; i++ {
// 生产者协程,生产 0-9 数字,生产的数字放入通道
src <- i
}
}()
// B 协程
go func() {
defer close(dest)
for i := range src {
// 消费者协程,消费操作为将生产者协程发送过来的数据进行平方
// 消费结果放入 dest 通道
dest <- i * i
}
}()
// 主线程从通道中输出最终的平方结果
for i := range dest {
fmt.Println(i)
}
}
如果生产者的逻辑比较简单,消费者的逻辑比较复杂,那么生产者生产的速度比较快,消费者消费的速度比较慢,此时生产者和消费者之间如果用不带缓冲的 Channel 就会导致比较快的生产者要等待消费者消费,数据被消费后才能再进行生产。而使用带缓冲的 Channel 就可以解决这个速度不一致的问题。
Lock
Go 提倡通过通信实现共享内存,即 Channel 的机制,但 Go 也保留了通过共享内存实现通信,即 Lock 的机制。
var (
x int64
lock sync.Mutex // 锁
)
// 对变量执行 2000 次 +1 操作
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 Add() {
x = 0
for i := 0; i < 5; i++ {
// 开启 5 个协程,操作共享数据(不加锁)
go addWithoutLock()
}
// 等待 2s 让协程执行结束,再打印不加锁下的结果
time.Sleep(time.Second * 2)
fmt.Println("WithoutLock: ", x)
x = 0
for i := 0; i < 5; i++ {
// 开启 5 个协程,操作共享数据(加锁)
go addWithLock()
}
// 等待 2s 让协程执行结束,再打印加锁下的结果
time.Sleep(time.Second * 2)
fmt.Println("WithLock: ", x)
}
等待某个协程执行完成——并发任务的同步
在sync.WaitGroup
下会维护一个计数器来实现。其中的方法:
Add(delta int)
:计数器加delta
Done()
:计数器减一Wait()
:阻塞直到计数器为0
func ManyGoWait() {
var wg sync.WaitGroup
// 开启五个协程
wg.Add(5)
for i := 0; i < 5; i++ {
// 每个协程各打印0,1,2,3,4中的一个值
go func(j int) {
defer wg.Done() // 协程执行完毕的时候计数器减一
fmt.Println(j)
}(i)
}
// 主线程等待五个协程都执行完再继续往下执行
wg.Wait()
fmt.Println("五个协程都执行完毕.")
// do some ...
}