这是我参与「第五届青训营 」伴学笔记创作活动的第 3 天
GO为并发而生
高效的并发模型
并发是多个线程轮询使用一个CPU,并行是多个线程同时使用多个CPU。
并行的效率高于并发。
Go语言实现的调度模型,可以充分发挥多核性能,高效运行。
协程
简介
作用
原理
线程和线程区别
- 线程
- 昂贵的系统资源
- 栈MB级别
- 内核态
- 创建、切换、停止都是很重的操作。
- 昂贵的系统资源
- 协程
- 轻量级线程
- 栈KB级别
- 一次可以创建上万的协程
- 用户态
- 创建、切换、停止都是由GO语言完成
- 轻量级线程
使用
创建协程
调用函数时,前面加 go 关键字 例如:
// 快速打印 hello goroutine : 0~hello goroutine : 4
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)
}
通过 channel 通信
简介
通过通信来共享内存,而不是通过共享内存来通信。
这是 Go 线程通信的设计思想。
具体实现则是 Go 中的 channel。
channel 就像所有协程共享的队列,里面存放的消息先进先出,协程能往 channel 里存数据,也能从 channel 中取数据,可以让一个 gorountine 发送特定的值到另一个 goroutine。
创建 channel
API
make(chan 元素类型,[缓冲大小])
根据是否有缓冲区,可以分为无缓冲通道、有缓冲通道。
示例
// 创建一个无缓冲区的 channel
src := make(chan int)
// 创建一个缓冲为3个字节的 channel
dest := make(chan int, 3)
应用实例
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
}
}()
// 生产者往 src put 的逻辑简单,而 println 打印消费流逻辑复杂。
// 所以消费者消费速度慢,给 dest 设置缓冲区,避免生产者等着消费者消费而阻塞。
for i := range dest {
// 复杂操作
println(i)
}
}
通过临界区通信
简介
通过互斥量对内存进行加锁操作,同一时刻,只允许一个线程访问该内存区域。
缺点是多个 goroutine 访问临界区,会互相争抢。
创建临界区
API
使用 sync.Mutex 实例
// 创建 sync.Mutex 实例
lock sync.Mutex
// 加锁
lock.Lock()
// 临界区代码
// 解锁
lock.UnLock()
示例
var (
x int64
lock sync.Mutex
)
func addWithLock() {
for i := 0; i < 20000; i++ {
lock.lock()
x += 1
lock.UnLock()
}
}
func addWithoutLock() {
for i := 0; i < 2000; i++ {
x += 1
}
}
通过 WaitGroup 阻塞当前线程
简介
WaitGroup 计数器归零前,阻塞当前线程。
API
WaitGroup 实际上维护了一个计数器。
WaitGroup
Add(delta int) // 计数器 + delta
Done() // 计数器 - 1
Wait() // 阻塞直到计数器为0
实例
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()
}