Go入门系列

Go入门系列(九)并发

2020-03-17  本文已影响0人  RabbitMask

目录
一、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) 
上一篇下一篇

猜你喜欢

热点阅读