Goroutine浅析

GoroutineGo语言并发编程中的一个重要概念,事实上我们可以把它理解成为非常轻量级的线程(thread)或协程(coroutine)。每个运行的 Go程序都至少包含一个goroutine——运行main函数的goroutine

创建goroutine要使用go语句:

go function(arguments)
go func(parameters) { block } (arguments)

go后面要跟一个已经定义的函数或是一个现定义的匿名函数。执行完这条语句后,就会启动一个独立的,和其它程序中已有的goroutine并行运行的新的goroutine

runtime程序包中提供了一个GOMAXPROCS函数,它可以用来设定实际上有多少个操作系统线程来执行Go程序。截止到目前为止(1.4.2版本),这个值一直默认为1。从1.5版本起,Go的开发者打算把这个默认值改为可以使用的CPU个数,并认为这会改善Go程序的性能。看一个简单的程序:

package main

import (
        "fmt"
        "time"
        "runtime"
)

func sleep(s string) {
        for {
                fmt.Println(s)
                time.Sleep(time.Second)
        }
}

func main() {
        fmt.Println(runtime.GOMAXPROCS(0))
        go sleep("Hello")
        go sleep("World")
        time.Sleep(1000 * time.Second)
}

main函数第一行打印了当前系统默认设置的GOMAXPROCS的值。另外在main goroutine里启动了两个goroutine,分别交替地打印“Hello”和“World”。执行如下:

1
Hello
World
Hello
World
Hello
World
Hello
World
......

可以看到默认的GOMAXPROCS的值的确为1。“Hello”和“World”也被交替打印了。在执行过程中,使用pstack命令查看进程的线程调用栈:

[root@Fedora ~]# pstack 10773
Thread 1 (LWP 10773):
#0  runtime.futex () at /usr/local/go/src/runtime/sys_linux_amd64.s:278
#1  0x000000000042b197 in runtime.futexsleep () at /usr/local/go/src/runtime/os_linux.c:49
#2  0x000000000040adae in runtime.notesleep (n=0x54df18 <runtime.m0+216>) at /usr/local/go/src/runtime/lock_futex.go:145
#3  0x000000000042e669 in stopm () at /usr/local/go/src/runtime/proc.c:1178
#4  0x000000000042f4c2 in findrunnable () at /usr/local/go/src/runtime/proc.c:1487
#5  0x000000000042f881 in schedule () at /usr/local/go/src/runtime/proc.c:1575
#6  0x000000000042fb03 in runtime.park_m () at /usr/local/go/src/runtime/proc.c:1654
#7  0x000000000041f94a in runtime.mcall () at /usr/local/go/src/runtime/asm_amd64.s:186
#8  0x000000000054dab0 in runtime.g0 ()
#9  0x000000000042de04 in runtime.mstart () at /usr/local/go/src/runtime/proc.c:836
#10 0x000000000041f84c in runtime.rt0_go () at /usr/local/go/src/runtime/asm_amd64.s:106
#11 0x0000000000000001 in ?? ()
#12 0x00007ffd43803348 in ?? ()
#13 0x0000000000000001 in ?? ()
#14 0x00007ffd43803348 in ?? ()
#15 0x0000000000000000 in ?? ()

可以看到的确只有一个线程在运行。

Goroutine的调度器(scheduler)并不是完全抢占的(fully-preemptive),当有GOMAXPROCSgoroutine并行运行时,运行的goroutine必须要放弃执行权,其它goroutine才有机会得到运行(比方说,调用time.Sleep())。此外,取决于调度器的实现,goroutine可能在执行任何函数时,都会放弃CPU。看下面这个程序:

package main

import (
    "fmt"
)

func say(s string) {
    for i := 0; i < 5; i++ {
        fmt.Println(s)
    }
}

func main() {
    go say("world")
    say("hello")
}

执行结果如下:

hello
hello
hello
hello
hello

main goroutine运行起来以后,并没有给另外一个goroutine打印“world”的机会。随着main goroutine执行完毕,这个程序也退出了。修改程序如下:

package main

import (
    "fmt"
    "time"
)

func say(s string) {
    for i := 0; i < 5; i++ {
        fmt.Println(s)
    }
}

func main() {
    go say("world")
    say("hello")
    time.Sleep(time.Second)
}

这次执行结果如下:

hello
hello
hello
hello
hello
world
world
world
world
world

main函数最后加了一行time.Sleep(time.Second),这样main goroutine就可以把CPU的执行权让出来,所以另外一个goroutine得以运行。

参考资料:
(1)Go 1.5 GOMAXPROCS Default
(2)Some questions about goroutine
(3)Why is time.sleep required to run certain goroutines