12、go的协程和管道
协程概念
又称微线程,纤程,协程是一种用户态的轻量级线程
作用:在执行A函数时候,可以随时中断,去执行B函数,然后中断继续执行A函数(可以自动切换),这一切换过程并不是函数调用(没有调用语句),过程很像多线程,然而协程中只有一个线程在执行(一个线程中有多个协程)
代码案例:开启一个协程非常简单,调用函数时候前面加个go就好了
func test() {
for i := 0; i < 10; i++ {
fmt.Println("hello golang + ", strconv.Itoa(i))
// 阻塞1秒
time.Sleep(time.Second)
}
}
func main() { // 主线程
// 开启一个协程
go test()
// 主线程也打印10次
for i := 0; i < 10; i++ {
fmt.Println("hello 王彪 + ", strconv.Itoa(i))
// 阻塞1秒
time.Sleep(time.Second)
}
}
主死从随
主线程和协程执行流程:
验证代码:让协程执行的久一点,主线程结束的早一点
func test() {
// 从线程打印100次
for i := 0; i < 100; i++ {
fmt.Println("hello golang + ", strconv.Itoa(i))
// 阻塞1秒
time.Sleep(time.Second)
}
}
func main() { // 主线程
// 开启一个协程
go test()
// 主线程也打印10次
for i := 0; i < 10; i++ {
fmt.Println("hello 王彪 + ", strconv.Itoa(i))
// 阻塞1秒
time.Sleep(time.Second)
}
}
打印:
主线程结束了,所以协程也结束了,可以看到协程还多打印了一次,说明在主线程结束的一瞬间,协程还可以苟延残喘一下。
另一种情况,如果协程执行的很快,主线程还没结束,那就正常协程结束就好了,主线程继续执行。
启动多个协程
var w sync.WaitGroup // 只定义无需赋值
func main() {
w.Add(5)
for i := 0; i < 5; i++ {
// 启动五个协程,匿名函数
go func(n int) {
defer w.Done()
// 因为闭包的概念,如果这里不加参数接收的话,直接打印i,可能会打印5,
fmt.Println(n) // 乱序打印0到4
}(i)
}
// 主线程要等待,不然协程很可能没执行完就被主死从随
w.Wait()
}
多协程操作同一数据
先看问题代码:
// 定义一个变量
var totalNum int
var wg sync.WaitGroup // 只定义无需赋值
func add() {
defer wg.Done() // 每次启动完协程以后,就-1
for i := 0; i < 100000; i++ {
totalNum = totalNum + 1
}
}
func sub() {
defer wg.Done() // 每次启动完协程以后,就-1
for i := 0; i < 100000; i++ {
totalNum = totalNum - 1
}
}
func main() {
wg.Add(2) // 每次启动就+2
// 启动协程
go add()
go sub()
wg.Wait() // 主线程一直阻塞,什么时候协助执行完了,wg就减成0了,就停止阻塞
fmt.Println(totalNum)
}
正常来说,最后会打印0,但其实不是,每次执行都是不同的数据且不是0
问题出错的可能原因:
解决问题:加入互斥锁
使用互斥锁同步协程
// 定义一个变量
var totalNum int
var wg sync.WaitGroup // 只定义无需赋值
// 加入互斥锁
var lock sync.Mutex
func add() {
defer wg.Done() // 每次启动完协程以后,就-1
for i := 0; i < 100000; i++ {
// 加锁
lock.Lock()
totalNum = totalNum + 1
// 解锁
lock.Unlock()
}
}
func sub() {
defer wg.Done() // 每次启动完协程以后,就-1
for i := 0; i < 100000; i++ {
lock.Lock()
totalNum = totalNum - 1
lock.Unlock()
}
}
func main() {
wg.Add(2) // 每次启动就+2
// 启动协程
go add()
go sub()
wg.Wait() // 主线程一直阻塞,什么时候协助执行完了,wg就减成0了,就停止阻塞
fmt.Println(totalNum)
}
运行多遍,发现都为0,没问题
WaitGroup控制协程退出
var wg sync.WaitGroup // 只定义无需赋值
func main() {
for i := 0; i < 5; i++ {
wg.Add(1) // 协程开始时候+1
go func(n int) {
fmt.Println(n)
wg.Done() // 协程执行完-1
}(i)
}
// 主线程一直阻塞,等到wg减到0时候,就停止阻塞
wg.Wait()
}
读写锁
读的时候,数据之间不受影响,写和读的时候就会受影响
var wg sync.WaitGroup
// 加入互斥锁
var rwLock sync.RWMutex
func read() {
defer wg.Done()
rwLock.RLock() // 读数据锁不产生影响
fmt.Println("开始读数据")
time.Sleep(time.Second)
fmt.Println("读取数据成功")
rwLock.RUnlock()
}
func write() {
defer wg.Done()
rwLock.Lock()
fmt.Println("开始写数据")
time.Sleep(time.Second * 10)
fmt.Println("数据写成功")
rwLock.Unlock()
}
func main() {
wg.Add(6) // 每次启动就+2
// 测试读多写少
for i := 0; i < 5; i++ {
go read()
}
go write()
wg.Wait() // 主线程一直阻塞,什么时候协助执行完了,wg就减成0了,就停止阻塞
}
允许代码后测试发现:写数据时候锁生效,会阻塞,读的时候是可以并行的
管道
管道就是数据结构-队列,数据是先进先出,自身也线程安全的,多个协程访问时不用加锁,管道也有数据类型的。
管道定义好容量后,不能多存入或者多取出数据,会报错
func main() {
// 定义一个int类型的管道
var intChan chan int
// 通过make初始化,管道可以存放3个int类型的数据
intChan = make(chan int, 3)
// 证明管道是引用类型
fmt.Printf("intChan的值:%v", intChan) // 0xc00008e000
// 给管道里存放数据
intChan <- 10
num := 20
intChan <- num
//intChan <- 40
// 不能存放大于容量的数据
//intChan <- 80
// 输出管道的长度
fmt.Printf("管道实际长度:%v, 管道容量:%v", len(intChan), cap(intChan))
fmt.Println()
// 取出管道里的值
num1 := <-intChan
fmt.Println(num1) // 10
num2 := <-intChan
fmt.Println(num2) // 20
}
管道的关闭
管道关闭后,可以取数据,但不可以往管道里存放数据了
func main() {
// 定义一个int类型的管道
var intChan chan int
// 通过make初始化,管道可以存放3个int类型的数据
intChan = make(chan int, 3)
// 给管道里存放数据
intChan <- 10
intChan <- 20
// 关闭管道
close(intChan)
// 再次往里面写入数据,会报错
//intChan <- 30
// 管道关闭后读数据,可以正常使用
fmt.Println(<-intChan) // 10
}
管道的遍历
func main() {
// 定义一个int类型的管道
var intChan chan int
intChan = make(chan int, 100)
// 往管道放100个数据
for i := 0; i < 100; i++ {
intChan <- i
}
// 遍历管道前要先关闭管道,不然遍历管道结束后还会遍历(读取管道数据),就会报错
close(intChan)
//遍历管道
for v := range intChan {
fmt.Println(v)
}
}
协程和管道协同工作
var wg sync.WaitGroup
// 写
func writeData(intChan chan int) {
defer wg.Done()
for i := 0; i < 50; i++ {
intChan <- i
fmt.Println("写入的数据:", i)
time.Sleep(time.Second)
}
close(intChan)
}
// 读
func readData(intChan chan int) {
wg.Done()
// 遍历
for v := range intChan {
fmt.Println("读取的数据:", v)
time.Sleep(time.Second)
}
}
func main() {
// 写协程和读协程共同操作同一个管道
// 定义管道
ichan := make(chan int, 50)
// 开启读和写的协程
wg.Add(2)
go writeData(ichan)
go readData(ichan)
wg.Wait()
}
只读只写管道
默认情况下,管道是可读可写的,我们也可以声明为只写或只读的
func main() {
// 声明只写
var intChan2 chan<- int
intChan2 = make(chan int, 3)
intChan2 <- 10
// 读操作时候,是报错的
//num := <-intChan2
fmt.Println("intChan2:", intChan2)
// 声明只读
var intChan3 <-chan int
// 允许会报错
//num := <-intChan3
//fmt.Println(num)
// 可以这样不让报错
if intChan3 != nil {
num := <-intChan3
fmt.Println(num)
}
}
管道的阻塞
只写不读,容量满了还在写,就会出现阻塞错误,如果读和写效率一月,甚至读的慢写的快,都不会出现阻塞
select
解决多个管道选择问题,随机公平的选择一个管道执行,
case后面必须写io操作,不能是等值
default防止select阻塞
func main() {
intCh := make(chan int, 1)
go func() {
time.Sleep(time.Second * 5)
intCh <- 10
}()
// 再定义一个string类型的管道
stringCh := make(chan string, 1)
go func() {
time.Sleep(time.Second * 2)
stringCh <- "彪哥学golang"
}()
select { // 多路复用,随机选一个效率高的,stringCh休眠时间短,所以会打印stringCh
case v := <-intCh:
fmt.Println("intCh:", v)
case v := <-stringCh:
fmt.Println("stringCh:", v)
default: // 加上default防止select阻塞,有阻塞直接执行default里面的了
fmt.Println("防止select阻塞")
}
}
defer + recover 处理异常
// 输出数字
func printNum() {
for i := 0; i < 10; i++ {
fmt.Println(i)
}
}
// 做除法操作
func devide() {
defer func() {
err := recover()
if err != nil {
fmt.Println("除法错误:", err)
}
}()
num1 := 10
num2 := 0
result := num1 / num2
fmt.Println(result)
}
func main() {
go printNum()
go devide() // 此协程虽然出现异常,但程序不会报错停止,上面的协程还是会正常打印0-9
time.Sleep(time.Second * 5)
}
《三体》中有句话——弱小和无知不是生存的障碍,傲慢才是。
所以我们不要做一个小青蛙