Java转Go—14并发编程

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()

运行结果:

Java转Go—14并发编程
运行结果

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)
}

运行结果:

Java转Go—14并发编程
运行结果

这样就会先打印出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 。

运行结果:

Java转Go—14并发编程
运行结果

每一次打印的数字的顺序都应该是不一样的,因为一共启动了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())
}

运行结果:

Java转Go—14并发编程
运行结果

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")
}

运行结果:

Java转Go—14并发编程
运行结果

当程序运行到 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,然后再才打印另外一个线程。(无论尝试多少次都是这样)

运行结果:

Java转Go—14并发编程
运行结果

使用默认线程数( 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交替打印的情况。

运行结果:

Java转Go—14并发编程
运行结果


原文始发于微信公众号(良猿):Java转Go—14并发编程

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。

文章由极客之音整理,本文链接:https://www.bmabk.com/index.php/post/215808.html

(0)
小半的头像小半

相关推荐

发表回复

登录后才能评论
极客之音——专业性很强的中文编程技术网站,欢迎收藏到浏览器,订阅我们!