写点什么

Go 语言入门 13—并发

作者:良猿
  • 2022-11-05
    湖北
  • 本文字数:2920 字

    阅读完需:约 10 分钟

Go语言入门13—并发

goroutine

goroutine(协程)是 go 语言中独有的一种用于并发编程的机制,在 Java 或 C++中,如果需要实现并发编程,通常需要我们自己维护一个线程池,然后将需要并发的内容包装成一个个的任务放到线程池中去执行,但是在 go 语言中就不一样,goroutine 是由 go 的运行时调度和管理的。go 程序会智能地将 goroutine 中的任务合理地分配给每个 CPU 去执行,不需要编程人员操心任务调度等问题。

使用 goroutine

当某个任务需要并发执行时,我们只需要将并发执行的任务包装成一个函数,开启一个 goroutine 去执行该函数即可,开启 goroutine 就是在函数调用前加上go关键字。


代码示例:


package main
import "fmt"
func main() { testGoroutine() fmt.Println("test main...")}
func testGoroutine() { fmt.Println("test goroutine...")}
复制代码


上述代码是没有开启 goroutine 进行并发的代码,所以在代码运行时,testGoroutine方法和main方法串行执行,依次打印test goroutine...和test main...


接下来在 testGoroutine 函数调用之前加上关键字go


go testGoroutine()
复制代码


运行结果:



testGoroutine函数调用前加上go关键字后反而只打印出了test main...,这是因为程序在启动时首先会为 main 函数创建一个默认的 goroutine,当 main 函数执行完的时候该 goroutine 也就结束了,所以在 main 函数中启动的 goroutine 也就会随着 main 函数结束而一同结束。


要解决这个只需要在 main 函数中加上time.Sleep,让 main 函数等一会,给 testGoroutine 充分的时间执行即可。


func main() {    go testGoroutine()    fmt.Println("test main...")    time.Sleep(time.Second)}
复制代码


运行结果:



这样就会先打印出test main...,然后再打印test goroutine...,因为通过关键字 go 启动 testGoroutine 函数时,创建 goroutine 会花费一定的时间,但是 main 是在已经创建好的 goroutine 中直接执行的,所以就会先打印出test main...


使用 go 语言并发编程就是这么简单,同时还可以启动多个 goroutine。


代码示例:


package main
import ( "fmt" "time")
func main() { for i := 0; i < 10; i++ { go testGoroutine(i) } fmt.Println("test main...") time.Sleep(time.Second)}
func testGoroutine(i int) { fmt.Println("test goroutine...", i)}
复制代码


代码通过 for 循环启动 10 个 goroutine ,分别在每个 goroutine 中都打印传入的变量 i 。


运行结果:



每一次打印的数字的顺序都应该是不一样的,因为一共启动了 10 个 goroutine ,这 10 个 goroutine 是并发执行的,而且 goroutine 是随机调度的。


go 关键字除了使用在调用函数之前,还可以将 go 关键字和函数的声明写到一起。


代码示例:


package main
import ( "fmt" "time")
func main() { for i := 0; i < 10; i++ { go func(i int) { fmt.Println("test goroutine...", i) }(i) } fmt.Println("test main...") time.Sleep(time.Second)}
复制代码


使用该写法同样可以正常启动 10 个 goroutine 并发,代码正常运行。

runtime 包

runtime 包是 go 语言并发编程中很重要的一个包,这个包里面包含了很多系统资源、并发操作的函数。

系统资源获取

  • runtime.GOOS:获取操作系统类型

  • runtime.GOARCH:获取操作系统内核

  • runtime.NumCPU():获取操作系统核心数

  • runtime.NumGoroutine():获取当前正在执行和排队的任务总数


代码示例:


func main() {    fmt.Println("操作系统:",runtime.GOOS)    fmt.Println("操作系统内核:",runtime.GOARCH)    fmt.Println("操作系统核心数:",runtime.NumCPU())    fmt.Println("任务总数:",runtime.NumGoroutine())}
复制代码


运行结果:


runtime.Gosched()

runtime.Gosched()意味着当前 goroutine 让出 CPU,供其它的goroutine获得执行的机会。同时,当前的goroutine也会在未来的某个时间点继续运行,假设有一下代码:


func main() {    go func() {        for i := 0; i < 2; i++ {            fmt.Println("test")        }    }()    fmt.Println("main")}
复制代码


在这个代码中,大部分情况下都是会打印出 main ,然后程序就正常结束了,具体原因上面有说到,可以通过 time.Sleep(time.Second) 来控制 main 函数等待子协程执行完毕,同时我们还可以使用 runtime.Gosched() 让 main 主协程让出 CPU,供子协程获得执行的机会。


代码示例:


func main() {    go func() {        for i := 0; i < 2; i++ {            fmt.Println("test")        }    }()    runtime.Gosched()    // main 协程让出CPU,供子协程获得执行的机会    fmt.Println("main")}
复制代码


运行结果:



当程序运行到 runtime.Gosched() 时 main 会让出 CPU,所以就会先打印出两个 test 然后再打印出 main 。

runtime.Goexit()

结束当前goroutine,其他的goroutine不受影响,主程序也一样继续运行,但是在结束 goroutine 时,会先执行该 goroutine 中的 defer 语句。


代码示例:


func main() {    // goroutine A    go func() {        defer fmt.Println("defer")        runtime.Goexit()        fmt.Println("goroutine A")    }()    // goroutine B    go func() {        fmt.Println("goroutine B")    }()    time.Sleep(time.Second)    fmt.Println("main")}
复制代码


运行结果:



上述代码所示,一共启动了两个 goroutine ,在 goroutine A 中调用的 runtime.Goexit() ,所以 goroutine A 会退出,但是在退出之前会先执行 defer 语句的内容,结束语句之后的打印语句就不会再执行。虽然 goroutine A 退出了,但是 goroutine B 和 main 主协程都不会受到影响。

runtime.GOMAXPROCS

runtime.GOMAXPROCS 指定使用多少个操作系统线程来执行当前的 go 语言程序,在 go1.5 之前默认使用的单核执行,在 go1.5 之后,默认使用的当前系统的 CPU 核心数,例如当前系统是 8 核心,则就会将代码同时调度到 8 个操作系统线程上执行。


使用一个线程:


func main() {    runtime.GOMAXPROCS(1)    go func() {        for i := 0; i < 10; i++ {            fmt.Println("线程1:", i)        }    }()
go func() { for i := 0; i < 10; i++ { fmt.Println("线程2:", i) } }()
time.Sleep(time.Second)}
复制代码


当把 GOMAXPROCS 设置成 1 时,在代码中同时启动两个 goroutine ,则系统会先将一个任务执行完成之后才会执行第二个任务,所以当前程序打印的结果中,永远都是一个线程从 0 打印到 9,然后再才打印另外一个线程。(无论尝试多少次都是这样)


运行结果:



使用默认线程数( go 版本为 1.17 所以默认为操作系统核心数)


func main() {    go func() {        for i := 0; i < 10; i++ {            fmt.Println("线程1:", i)        }    }()
go func() { for i := 0; i < 10; i++ { fmt.Println("线程2:", i) } }()
time.Sleep(time.Second)}
复制代码


由于没有设置 GOMAXPROCS ,所以使用了默认的线程数,也就是当前系统的核心数,这样就会将代码调度到多个操作系统线程上执行,和上面不一样的是如果尝试执行多次,就会出现线程 1 和线程 2 交替打印的情况。


运行结果:



发布于: 刚刚阅读数: 4
用户头像

良猿

关注

还未添加个人签名 2019-02-13 加入

还未添加个人简介

评论

发布
暂无评论
Go语言入门13—并发_Go_良猿_InfoQ写作社区