Go入门系列(九)并发
目录
一、goroutine
二、channel
Go语言的特色之一就是并发,Go 从语言层面就支持并发。同时实现了自动垃圾回收机制。并且Go语言的并发机制运用起来非常简便,在启动并发的方式上直接添加了语言级的关键字就可以实现,和其他编程语言相比更加轻量。
一、goroutine
轻量级线程goroutine,便是Go并发的核心。
goroutine 语法格式:
go 函数名( 参数列表 )
我们依然以python并发教程的“吃饭睡觉打豆豆”为例:
func eat() {
fmt.Println("开始吃饭:",time.Now())
time.Sleep(time.Second*1)
fmt.Println("吃饭结束:",time.Now())
}
func sleep() {
fmt.Println("开始睡觉:",time.Now())
time.Sleep(time.Second*1)
fmt.Println("睡觉结束:",time.Now())
}
func hit() {
fmt.Println("开始打豆豆:",time.Now())
time.Sleep(time.Second*1)
fmt.Println("豆豆已死:",time.Now())
}
func main() {
eat()
sleep()
hit()
}
#输出
开始吃饭: 2020-03-17 18:53:19.5784287 +0800 CST m=+0.003989301
吃饭结束: 2020-03-17 18:53:20.5937139 +0800 CST m=+1.019274501
开始睡觉: 2020-03-17 18:53:20.5937139 +0800 CST m=+1.019274501
睡觉结束: 2020-03-17 18:53:21.5940398 +0800 CST m=+2.019600501
开始打豆豆: 2020-03-17 18:53:21.5940398 +0800 CST m=+2.019600501
豆豆已死: 2020-03-17 18:53:22.594365 +0800 CST m=+3.019925601
可以看到,因为每个函数耗时1s,最终用时3s。
我们尝试进行并发改写,唉?为什么是尝试:
func main() {
go eat()
go sleep()
go hit()
}
#输出
恭喜翻车!没有输出!why?可能网上的教程大多在这里巧合的把坑给避开了,我们知道线程在进程结束时也会退出,也就是说如果进程瞬间终止,我们的线程没来得及运行就停止了。
我们来证实一下:
func main() {
go eat()
go sleep()
go hit()
time.Sleep(time.Second*2)
}
#输出
开始吃饭: 2020-03-17 19:15:38.0483548 +0800 CST m=+0.003989401
开始打豆豆: 2020-03-17 19:15:38.0483548 +0800 CST m=+0.003989401
开始睡觉: 2020-03-17 19:15:38.0483548 +0800 CST m=+0.003989401
睡觉结束: 2020-03-17 19:15:39.0666338 +0800 CST m=+1.022268301
豆豆已死: 2020-03-17 19:15:39.0666338 +0800 CST m=+1.022268301
吃饭结束: 2020-03-17 19:15:39.0666338 +0800 CST m=+1.022268301
三个行为并发执行,耗时1s,大家也应该觉察出问题了,我们缺少一个阻塞,类似线程守护的思想,我们需要在线程完全执行完毕时才允许主进程的退出,在此之前,进行阻塞。
二、channel
channel通道类型,是Go语言在语言级别提供的 goroutine 间的通信方式。我们可以使用 channel 在两个或多个 goroutine 之间传递消息。
声明一个通道类型,通道可以设置缓冲区,通过 make 的第二个参数指定缓冲区大小,不过可以忽略:
ch := make(chan int)
ch := make(chan int, 100)
然后我们就可以来利用通道传值:
ch <- v // 把 v 发送到通道 ch
v := <-ch // 从 ch 接收数据并把值赋给 v
貌似有点抽象,我们借助个实例体会下,在传递有效值之前,我们先来解决一个上面流的伏笔,我们知道线程是不阻塞的,但是通道确实堵塞的,如果我们等待通道数据返回,这个程序就会被阻塞:
func eat(ch chan int) {
fmt.Println("开始吃饭:",time.Now())
time.Sleep(time.Second*1)
fmt.Println("吃饭结束:",time.Now())
ch <- 1
}
func sleep(ch chan int) {
fmt.Println("开始睡觉:",time.Now())
time.Sleep(time.Second*1)
fmt.Println("睡觉结束:",time.Now())
ch <- 2
}
func hit(ch chan int) {
fmt.Println("开始打豆豆:",time.Now())
time.Sleep(time.Second*1)
fmt.Println("豆豆已死:",time.Now())
ch <- 3
}
func main() {
ch := make(chan int, 3)
go eat(ch)
go sleep(ch)
go hit(ch)
x,y,z:=<-ch,<-ch,<-ch
fmt.Println(x,y,z)
}
#输出
开始打豆豆: 2020-03-17 20:10:26.1247241 +0800 CST m=+0.003989601
开始吃饭: 2020-03-17 20:10:26.1247241 +0800 CST m=+0.003989601
开始睡觉: 2020-03-17 20:10:26.1247241 +0800 CST m=+0.003989601
豆豆已死: 2020-03-17 20:10:27.1400096 +0800 CST m=+1.019275101
睡觉结束: 2020-03-17 20:10:27.1400096 +0800 CST m=+1.019275101
吃饭结束: 2020-03-17 20:10:27.1400096 +0800 CST m=+1.019275101
3 2 1
我们可以看到,不需要sleep,进程成功阻塞,当然例子中的传值无意义,只是把这个阻塞过程具象一下,我们该写下上一章提到的用户信息收集的例子,借助通道改写。
func main() {
ch := make(chan string, 3)
go getCurrent(ch)
fmt.Print("查询ing......\n")
fmt.Printf("username:%s\nuid:%s\nhomedir:%s",<-ch,<-ch,<-ch)
}
func getCurrent(ch chan string){
u, err := user.Current()
checkErr(err)
ch <- u.Username
ch <- u.Uid
ch <- u.HomeDir
}
func checkErr(err error) {
if err != nil {
fmt.Println(err.Error())
return
}
}
#输出
查询ing......
username:GAMING\Administrator
uid:S-1-5-21-288493977-1313606145-3801498405-500
homedir:C:\Users\Administrator
End
Go语言的并发机制,可能是接触的语言中最简单的。
另外,我们再简单补充一点,许多语言(特指python2333)都有个很诡异的地方,多并发的时候常常会发生一核满载七核外观的场面,在go语言中也会出现类似的情况,毕竟编译器不可能那么智能,当然解决方案是有的。
通过NumCPU()我们可以获得当前设备的cpu核心数:
import (
"fmt"
"runtime"
)
func main() {
cpuNum := runtime.NumCPU()
fmt.Println("cpu核心数:", cpuNum)
}
然后我们就可以对应设置goroutine需要用到的cpu数量,当然也可以使用系统环境变量设置,永久改变。
runtime.GOMAXPROCS(cpuNum)