一. goroutine
很多语言在进行并发编程的时候需要维护一个线程池,包装一个个的任务,并且需要自己去调度线程执行任务维护上下文的切换,而go语言为了我们提供了goroutine,我们可以定义很多的任务,让系统帮助我们把这些任务分配到cpu上实现并且执行,gorotinue类似于线程的概念,但是它是由go的运行时(runtime)调度和管理的,go程序会将goroutine中的任务合理地分配给每个cpu,在语言层面内置了调度和上下文切换的机制;在go语言的并发编程中我们只需要将任务包装成一个函数,开启一个goroutine去执行这个函数。如何使用goroutine呢?其实很简单,我们只需要在调用函数(普通函数和匿名函数)的前面加上go关键字,就可以为一个函数创建一个goroutine,一个goroutine对应一个函数,使用go关键字调用则开启了一个协程:
package main
import "fmt"
func f() {
fmt.Println("f goroutine!")
}
func main() {
// 开启一个goroutine执行函数f()
go f()
fmt.Println("main goroutine")
}
上面的例子中我们在调用f()函数的前面加上go关键字,开启一个goroutine去执行这个函数,但是发现只是打印了main函数中的字符串,没有打印f f goroutine!,为什么呢?其实在程序启动的时候go语言就为main()函数创建了一个默认的goroutine,当main()函数返回的时候该goroutine就结束了,所有在main()函数中启动的goroutine会一同结束,如何解决呢?一个最简单的一个方法是让main()函数等一下f()函数,可以使用time.sleep()函数让当前main()函数的goroutine停止一段时间,让f()函数的goroutine执行完,打印一个字符串可以在1s之内完成所以我们在控制台可以看到输出了两个字符串:
package main
import (
"fmt"
"time"
)
func f() {
fmt.Println("f goroutine!")
}
func main() {
go f()
fmt.Println("main goroutine")
// 让main()函数所在的goroutine阻塞一段时间让f()函数执行完
time.Sleep(time.Second)
}
启动多个goroutine
通常一次会执行多个任务,那么这个时候就需要启动多个goroutine执行多个任务,下面的例子使用了sync.WaitGroup来实现goroutine的同步,每一次执行上面的代码发现输出的i都是不一样的,这是因为这10个goroutine是并发执行的,由goroutine的调度机制决定先执行哪个goroutine,而且在main函数中语句是从上往下执行的,而对于使用go关键字调用的函数是并发执行的,由go语言的调度机制决定执行哪一个goroutine,反正需要记住的一点是如果需要并发执行多个任务,则需要使用go关键字开启一个goroutine来执行一个任务,并且是由go语言的调度机制决定当前执行哪个goroutine,如果没有并发操作则正常调用函数即可:
package main
import (
"fmt"
"sync"
)
var wg sync.WaitGroup
func hello(i int) {
defer wg.Done() // 当前函数执行完返回的时候将当前登记的goroutine-1
fmt.Println("Hello Goroutine!", i)
}
func main() {
for i := 0; i < 10; i++ {
wg.Add(1) // 启动一个goroutine就登记+1
go hello(i)
}
wg.Wait() // 等待所有登记的goroutine都结束
}
go语言中goroutine与操作系统线程之间的关系:一个操作系统线程对应用户态多个goroutine;go语言程序可以同时使用多个操作系统线程,goroutine与操作系统线程是多对多的关系。
二. runtime
1. runtime.Gosched()函数,让出cpu时间片,允许其他的goroutine执行,这个方法不会暂停当前的goroutine,所以在后面当前的goroutine会自动恢复运行:
package main
import (
"fmt"
"runtime"
)
func f() {
for i := 0; i < 3; i++ {
fmt.Println("hello")
}
}
func main() {
go f()
for i := 0; i < 3; i++ {
runtime.Gosched()
fmt.Println("world")
}
}
输出结果:
hello
hello
hello
world
world
world
2. runtime.Goexit()函数,结束当前的goroutine,不会影响到其他的goroutine,并且在结束当前的goroutine之前会运行完当前goroutine中所有的defer语句,如果在main()函数中调用Goexit()函数,当其他goroutine调用完成之后,由于main()函数没有返回最终导致整个程序崩溃:
package main
import (
"fmt"
"runtime"
"time"
)
func main() {
go func() {
defer fmt.Println("A.defer")
func() {
defer fmt.Println("B.defer")
// 结束协程
runtime.Goexit()
defer fmt.Println("C.defer")
fmt.Println("B")
}()
fmt.Println("A")
}()
time.Sleep(time.Second)
}
3. runtime.GOMAXPROCS,go语言运行的时候调度机制根据参数GOMAXPROCS确定需要使用多少个的操作系统线程来同时执行go语言代码,默认值是机器上的cpu核心数,在一个具有8个cpu核心的机器上,调度器会把go语言代码同时调度到8个操作系统线程上,可以通过参数GOMAXPROCS设置当前程序并发执行的占用的cpu核心数,如果设置为1那么两个goroutine只有一个cpu核心,此时是先做完一个任务再做另外一个任务,如果设置为2两个任务并发执行,可以使用runtime.NumCPU()函数查看电脑的cpu的核心数:
package main
import (
"fmt"
"runtime"
"time"
)
func a() {
for i := 1; i < 1000; i++ {
fmt.Println("A: ", i)
}
}
func b() {
for i := 1; i < 1000; i++ {
fmt.Println("B: ", i)
}
}
func main() {
runtime.GOMAXPROCS(2)
go a()
go b()
time.Sleep(time.Second)
}