Golang特色之一是支持高并发编程模型,以Goroutine作为基本的并发执行单元。
- Goroutine是轻量级线程
- Goroutine的调度是由Golang运行时进行管理的。
使用Goroutine
Golang使用go
关键字开启Goroutine来支持并发编程,使用go
语句会开启一个全新的运行期线程即Goroutine。
在调用函数前添加go
关键字即可为函数创建一个Goroutine
go funcName(argumentList)
- 一个函数可以创建出多个
goroutine
,而一个goroutine
必定对应一个函数。 - 同一个函数中所有Goroutine将共享同一个地址空间。
例如:默认Go程序从main
入口函数开始按照串行方式从上到下依次执行的
在Windows和Linux出现之前的古老年代,程序员在开发时并没有并发的概念,因为命令式程序设计语言是以串行为基础的,程序会顺序执行的每一条指令,整个程序只有一个执行上下文,即一个调用栈和一个堆。
package main
import (
"fmt"
)
func test() {
fmt.Println("test goroutine")
}
//程序启动时会创建一个名为main的Goroutine去执行
func main() {
test()
fmt.Println("main goroutine")
}
$ go run main.go
test goroutine
main goroutine
例如:创建并发执⾏单元,Go调度器会将其安排到合适的系统线程上去执行。
并发意味着程序在运行时具有多个执行上下文,对应着多个调用栈。由于每个进程在运行时都具有自己的调用栈和堆,因此会拥有一个完整的上下文。而操作系统在调度进程时会保存被调度进程的上下文环境,等待进程获得时间片后,再恢复该进程的上下文到系统中。
package main
import (
"fmt"
)
func test() {
fmt.Println("test goroutine")
}
//程序启动时会创建一个名为main的Goroutine去执行
func main() {
go test() //开启一个单独的Goroutine去执行当前函数
fmt.Println("main goroutine")
}
$ go run main.go
main goroutine
为什么只输出了main goroutine
呢?由于main
函数执行到go test()
,此时会创建并发任务,而非执行并发操作。新建的并行任务单元会被放置到系统队列中,等待调度器安排合适的系统线程去获取执行权。接着main
函数线下执行输出打印main goroutine
。执行完毕main
函数结束,此时main
函数内的goroutine
会全部死亡。
形象地看,main
函数类似夜王,旗下的Goroutine类似它创建出来的小鬼。夜王一死,所有的小鬼全部跟着倒下。
如何才能让test
函数中的内容打印输出呢?让main
主函数延迟结束。
package main
import (
"fmt"
"time"
)
func test() {
fmt.Println("test goroutine")
}
//程序启动时会创建一个名为main的Goroutine去执行
func main() {
//开启一个单独的Goroutine去执行当前函数
go test()
fmt.Println("main goroutine")
//主函数休眠1秒延迟结束等待test函数执行完毕
time.Sleep(time.Second * 1)
}
$ go run main.go
main goroutine
test goroutine
Go程序启动时会为main()
函数创建一个默认的Goroutine即Main Goroutine,当main()
函数返回时Main Goroutine结束,所有在main()
函数中启动的Goroutine也会一起结束。
Goroutine和线程一样,主函数main()
的Goroutine并不会等待其它Goroutine结束。如果main()
函数中的Goroutine结束,其下的所有goroutine
都将结束。
需要注意的是,函数调用前添加go
关键字表示本次调用会在一个全新的Goroutine中并发执行,当被调用的函数return
返回时,当前的Work Goroutine也会自动结束。如果函数存在返回值,则返回值会被丢弃。
使用匿名函数创建Goroutine
go
关键字可以为匿名函数或闭包启动Goroutine
go func(argumentList) {
//function body
}(parameters)
例如:并行执行定时打印计数
package main
import (
"fmt"
"time"
)
func main() {
//并发执行计数器
go func() {
var count int
for {
count++
fmt.Println("tick", count)
time.Sleep(time.Second)
}
}()
//接收命令行输入
var input string
fmt.Scanln(&input)
}
例如:计数器每秒执行1次的同时等待用户输入
package main
import (
"fmt"
"time"
)
//每隔1秒打印依次计数器
func counter() {
var count int
//无限循环
for {
count++
fmt.Println("tick", count)
time.Sleep(time.Second) //延迟1秒
}
}
func main() {
//并发执行计数器
go counter()
//接收命令行输入
var input string
fmt.Scanln(&input)
}
Go程序启动时运行时runtime
会默认为main()
主函数创建一个goroutine
,在main()
主函数的goroutine
中执行go counter()
时,counter()
计数器函数的goroutine
被创建。counter()
函数在自己的goroutine
中执行。同时main()
主函数依然持续运行。两个goroutine
通过Go程序的调度机制同时在运行。
启动多个Goroutine
使用for
循环开启多个Goroutine并发执行
package main
import (
"fmt"
"time"
)
func test(i int) {
fmt.Printf("test goroutine %d\n", i)
}
//程序启动时会创建一个名为main的Goroutine去执行
func main() {
for i := 0; i < 100; i++ {
go test(i) //开启一个单独的Goroutine去执行当前函数
}
fmt.Println("main goroutine")
//主函数休眠1秒延迟结束等待test函数执行完毕
time.Sleep(time.Second * 1)
}
多次执行会发现每次执行的顺序都不一致,为什么呢?因为多个Goroutine并发执行时,其调度是随机的。
对于实际应用中,休眠1秒是可能完全不够,因为无法预知for
循环内代码运行时间的长短,因此不能简单粗暴地使用time.Sleep()
来完成等待操作。
可以通过sync.WaitGroup
等待组实现多个任务的同步,sync.WaitGroup
可以保证在并发环境中完成指定数量的任务。
package main
import (
"fmt"
"sync"
)
//创建等待组
var wg sync.WaitGroup
func test(i int) {
defer wg.Done() //Goroutine执行完毕则计数-1
fmt.Printf("test goroutine %d\n", i)
}
//程序启动时会创建一个名为main的Goroutine去执行
func main() {
for i := 0; i < 100; i++ {
wg.Add(1) //启动一个Goroutine登记一个
go test(i) //开启一个单独的Goroutine去执行当前函数
}
fmt.Println("main goroutine")
//等待所有登记的Goroutine执行完毕
wg.Wait()
}
网友评论