1. 语言进阶 —— 从并发编程的视角了解 go 语言的本质
1.1 Goroutine——协程
- 协程:用户态,轻量级线程,栈KB级别
- 线程:内核态,线程跑多个协程,栈MB级别,比较昂贵的系统资源
(课件上栈级别的内容可能有误,老师讲课时说的和根据“线程跑多个协程”可以推断)
go语言可以创建上万个协程。
- go语言中协程的调用:只需在函数调用的时候在函数前面加上一个 go 关键字来运行程序,就能够启动一个协程。
快速打印Goroutine案例,开启多个协程打印:
package main
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) { //调用时加上go关键字开启协程
hello(j)
}(i)
}
time.Sleep(time.Second) //阻塞,保证子协程退出之前主协程不会退出
}
/*
hello goroutine:4
hello goroutine:0
hello goroutine:1
hello goroutine:2
hello goroutine:3
*/
1.2 CSP (Communicating Sequential Process)
协程之间的通信。
- go提倡通过通信共享内存而不是通过共享内存实现通信
- 通道Channel:可以让一个Goroutine发送特定值到另一个Goroutine的通信机制,实现上述的基础
- 临界区:通过共享内存实现数据交换。需要互斥量对共享内存加锁,影响程序性能
1.3 Channel的具体操作
channel是一个引用类型,创建方法:make(chan 元素类型, [缓冲大小]),分是否有缓冲通道,分以下两种具体示例:
- 无缓冲通道:make(chan int)
- 有缓冲通道:make(chan int, 2)
无缓冲通道进行通信时,导致发送的 goroutine 和接收的 goroutine 同步化,因此也称为同步通道;解决同步问题的方式就是使用带有缓冲区的有缓冲通道,缓冲大小表示存储的容量。
channel使用的例子:
A子协程发送0~9数字
B子协程计算输入数字的平方
主协程输出最后的平方数
package main
func CalSquare(){
src := make(chan int) // 无缓冲的通道
// 带缓冲的通道,考虑到消费者的消费速度可能会比生产者慢
// 带缓冲的channel可以解决生产和消费不同步带来的执行效率额问题
dest := make(chan int, 3)
go func() { // A子协程
defer close(src)
for i := 0; i<10; i++ {
src <- i
}
}()
go func() { // B子协程
defer close(dest)
//通过 range 遍历src的数据,实现不同 channel 的通信
for i:= range src{
dest <- i*i
}
}()
for i:= range dest{ //主协程
//可能是一些复杂操作,用打印代替
println(i)
}
}
1.4 并发安全 Lock
通过共享内存实现通信的情况。
案例:对变量执行 5000 次 +1 操作,由 10 个协程并发执行。最终能够看到的结果是,随着+1执行数量 和 协程数量的增加,加法的执行结果准确性差距越大。加锁的方式一直准确,而不加锁的方式错误偏差越来越大。
package main
import (
"sync"
"time"
)
var (
x int64
lock sync.Mutex
)
func addWithLock() {
//加锁的情况
for i := 0; i < 5000; i++ {
// 加锁获取临界区的资源
lock.Lock()
x += 1
// 释放临界取区的资源
lock.Unlock()
}
}
func addWithoutLock() {
for i := 0; i < 5000; i++ {
x += 1
}
}
func add() {
x = 0
for i := 0; i < 10; i++ {
go addWithoutLock()
}
time.Sleep(time.Second)
println("add without lock:", x)
x = 0
for i:=0; i<10; i++{
go addWithLock()
}
time.Sleep(time.Second)
println("add with lock:", x)
}
1.5 线程同步 WaitGroup
一个能用于协程阻塞的标准库,存在于sync.WaitGroup位置。其中主要的几个函数(方法)即含义如下:
Add(delta int)计数器+deltaDone()计数器 -1Wait()阻塞直到计数器为0
其中,对计数器有如下的规定:开启协程 计数器+1;执行结束 计数器-1;主协程阻塞直到计数器为0。
对协程优化的案例:快速打印 hello goroutine
package main
import (
"fmt"
"sync"
)
func hello(i int) {
println("hello goroutine:" + fmt.Sprint(i))
}
func ManyWait() {
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()
}