这是我参与「第五届青训营 」伴学笔记创作活动的第2天。
前言
本篇其实是在写昨天的内容,Go语言的并发编程
Go语言进阶
并发与并行
简单来说,并行真正意义上的,同一个时间段内可以做不同的事。
而并发是通过某种调度方法,交替地做不同的事。
二者均可以提升执行效率,但并行使用的线程(Thread)在内存资源占用上要比并发使用的协程(Coroutine)高很多,使用并发更具有性价比。
Goroutine
代码层面上,Go使用go func即可创建一个协程,下面是一个简单的小程序,使用Go的并发编程打印Hello0-4:
//1.1
package main
import (
"fmt"
"time")
func PrintHello(i int) {
fmt.Println("Hello" + fmt.Sprint(i))
}
func main() {
for i := 0; i < 5; i++ {
go func(m int) {
PrintHello(m)
}(i)
}
time.Sleep(time.Second)
}
输出结果为:
Hello2
Hello1
Hello0
Hello4
Hello3
对上面的代码稍作解释: 1.func(){ //代码块 }() 这是一种匿名函数的声明方式,位数}尾的( )表示传入实参并调用。
2.time.second 调用time包里的Second方法,其返回值为1s。
3.time.Sleep(T) 使当前协程休眠T s,它的作用我们稍后解答。
实际上,这个程序里创建了6个协程。虽然没有以go func的形式声明,但此处的func main也为一个协程,并被称为主协程。其余的5个go func均为子协程。
那么说回time.Sleep方法,看看如果我们注释掉这一行会发生什么:
没有打印出任何东西,为什么?
实际上在Go中,主协程结束时(此处的func main),其子协程也会停止运行。由于所有的PrintHello()都是子协程调用的,所以程序没来得及打印出任何东西就结束了。 故time.Sleep()在此处的作用是让主协程“等”子协程1s,待子协程运行完成时,再结束主协程。
但是,在实际的编程中,我们一般情况下无法得知子协程运行所需要的时间,使用time.Sleep(T)极可能使其休眠过长或者过短,造成运行速度下降或者主协程结束过快的问题,因此,我们需要用到WaitGroup。
等待组(WaitGroup)
WaitGroup是sync包下的一种类型,需要用impo导入后再使用。我们来看看经由WaitGroup改造后的程序1.1:
//1.2
package main
import (
"fmt"
"sync")
var Clock sync.WaitGroup
func PrintHello(i int) {
fmt.Println("Hello" + fmt.Sprint(i))
}
func main() {
Clock.Add(5)
for i := 0; i < 5; i++ {
go func(m int) {
defer Clock.Done()
PrintHello(m)
}(i)
}
Clock.Wait()
}
输出结果仍为:
Hello2
Hello1
Hello0
Hello4
Hello3
稍微解释一下这些改造: 1.var Clock sync.WaitGroup 定义一个名为Clock的sync.WaitGroup类型
2.Clock.Add(T) 调用WaitGroup的Add方法,使WaitGroup的计数器+T(T可为负数)
3.defer Clock.Done() 调用WaitGroup的Done方法,使WaitGroup的计数器-1
4.Clock.Wait() 调用sync.WaitGroup的Wait方法阻塞直到WaitGroup计数器减为0。
5.defer关键字 使代码在函数末尾进行。(假设C语言中有defer关键字,那么你可以malloc()后马上写defer free(),以避免忘记释放内存报错,还是挺不错的,可惜没有)
此程序有一个主协程五个子协程,用Add方法使WaitGroup计数器+5,每结束一个子协程,Done方法会使计数器-1,在减为0之前,Wait方法都会使协线程阻塞,以达到在所有子协程结束之前主协程都不会结束的效果,并且不会像time.Sleep方法一样出现运行速度下降或者主协程结束过快的问题。
CSP(Communicating Sequential Processes)并发模型
如何在不同的协程中实现通信?Go语言中提供了两种方法:通过共享内存实现通信与通过通信实现共享内存。Go更推荐第二种,我们拿出来细说。
通过通信实现共享内存。
Channel可以实现协程与协程之间的数据通信。
ChannelName :=make(chan type,capacity)
//capacity为缓冲区,若不需要则省略
使用Channel发送/接受数据:
V := <- ChannelName
ChannelName <- V
for range同样可以像遍历数组、map一样遍历Channel读取数据
for v := range ChannelName {
fmt.Println(v)
}
使用close函数关闭Channel(当你不再需要使用Channel发送数据):
close(ChannelName)
我们可以用Channel设计一个程序:
// 1.4
package main
import "fmt"
func main() {
out := make(chan int)
square := make(chan int)
go func() {
for i := 0; i < 10; i++ {
out <- i
}
close(out)
}() //子协程1
go func() {
for i := range out {
square <- i * i
}
close(square)
}() //子协程2
for i := range square {
fmt.Println(i)
}
}
输出:
0
1
4
9
16
25
36
49
64
81
子协程1传入int 1-9给Channel out
子协程2从Channel out接受数据,平方后传入Channel square
主协程从Channel square 接受数据并打印
引用
分发
This work is licensed under CC BY-SA 4.0