一、Goroutine
多个线程可以属于同一个进程并共享内存空间。因为多线程不需要创建新的虚拟内存空间,所以它们也不需要内存管理单元处理上下文的切换,线程之间的通信也正是基于共享的内存进行的,与重量级的进程相比,线程显得比较轻量。
虽然线程比较轻量,但是在调度时也有比较大的额外开销。每个线程会都占用 1 兆以上的内存空间,在对线程进行切换时不止会消耗较多的内存,恢复寄存器中的内容还需要向操作系统申请或者销毁对应的资源,每一次线程上下文的切换都需要消耗 ~1us 左右的时间,但是 Go 调度器对 Goroutine 的上下文切换约为 ~0.2us,减少了 80% 的额外开销。
Go 语言的调度器通过使用与 CPU 数量相等的线程减少线程频繁切换的内存开销,同时在每一个线程上执行额外开销更低的 Goroutine 来降低操作系统和硬件的负载。
1-1 开启 Goroutine
go func_name(x, y, z)
package main
import (
"fmt"
"time"
)
func say(s string) {
for i := 0; i < 5; i++ {
time.Sleep(100 * time.Millisecond)
fmt.Println(s)
}
}
func main() {
go say("world")
say("hello")
}
二、进程不会等待所有的 Goroutines 执行结束
go 内使用 goroutines 所谓的轻量线程,实现并发。并且在同一个程序中,所有的 goroutines 共享同一个地址空间。
但是由于 goroutines 的效率高,所以程序未等到所有的 goroutines 完成就会结束。
package main
import (
"fmt"
"time"
)
func main() {
workerCount := 2
for i := 0; i < workerCount; i++ {
// 使用轻量线程 执行函数
go doit(i)
}
time.Sleep(1 * time.Second)
fmt.Println("all done!")
}
func doit(workerId int) {
fmt.Printf("[%v] is running\n",workerId)
time.Sleep(3 * time.Second)
fmt.Printf("[%v] is done\n",workerId)
}
/*
[0] is running
[1] is running
all done!
*/
一个最常见的解决方法是使用 WaitGroup 变量,sync.WaitGroup 能有效的阻塞代码
package main
import (
"fmt"
"sync"
)
func main() {
var wg sync.WaitGroup
done := make(chan struct{}) // 创建结构体通道
workerCount := 2
for i := 0; i < workerCount; i++ {
wg.Add(1)
go doit(i,done,wg)
}
close(done)
wg.Wait()
fmt.Println("all done!")
}
func doit(workerId int,done <-chan struct{},wg sync.WaitGroup) {
fmt.Printf("[%v] is running\n",workerId)
defer wg.Done()
// defer 确保 wg.Done() 在该函数执行完毕之后执行,用于解锁资源
<- done
fmt.Printf("[%v] is done\n",workerId)
}
/*
[0] is running
[0] is done
[1] is running
[1] is done
*/
但通常会出现死锁问题。
死锁:两个或两个以上的线程,因抢夺资源而造成相互等待的无解结果。
各个 worker 都得到了原始的 WaitGroup 变量的一个拷贝。所有 worker 都在等着对 wg 内存空间资源的修改权,所以当 worker 执行wg.Done()
时,并没有在主 goroutine 上的 WaitGroup 变量上生效。
fatal error: all goroutines are asleep - deadlock!
package main
import (
"fmt"
"sync"
)
func main() {
var wg sync.WaitGroup
// 注意 已经初始,非nil
done := make(chan struct{})
wq := make(chan interface{})
// 非缓冲信道,发送方会阻塞直到接收方从信道中接受了值
workerCount := 2
for i := 0; i < workerCount; i++ {
wg.Add(1)
go doit(i,wq,done,&wg)
}
for i := 0; i < workerCount; i++ {
wq <- i
}
close(done) // 关闭信道,禁止数据流入,但可读。
wg.Wait()
fmt.Println("all done!")
}
func doit(workerId int, wq <-chan interface{},done <-chan struct{},wg *sync.WaitGroup) {
fmt.Printf("[%v] is running\n",workerId)
defer wg.Done()
for {
// select 是 Go 中的一个控制结构,类似于用于通信的 switch 语句。每个 case 必须是一个通信操作,要么是发送要么是接收。
// select 能让 goroutine 同时等待多个 channel 的可读或可写
// 当满足多个 case 时,随机执行一个可运行的case 若没有 case 可执行将阻塞等待,直到存在可执行的case
select {
case m := <- wq:
fmt.Printf("[%v] m => %v\n",workerId,m)
case <- done:
/*
a,ok:= <- done
fmt.Printf("[%v] is done %v,%v\n",workerId,a,ok)
[1] is done {},false
*/
// 从一个nil channel中接收数据会一直被block
// 从关闭的 channel 中不但可以读取已发送的数据,还可以不断读取初始值
fmt.Printf("[%v] is done\n",workerId)
return
}
}
}
// 注意:执行结果不唯一
/*
[1] is running
[1] m => 0
[0] is running
[0] is done
[1] m => 1
[1] is done
all done!
*/
/*
[1] is running
[1] m => 0
[0] is running
[0] m => 1
[0] is done {}
[1] is done {}
all done!
*/
// 主线程执行程序,0 1 随机执行case,走到 done 即结束循环,走 wq 即读取
// 主线程同时执行 wq 的写入,并关闭信道,等待所有线程完成
三、for 循环的 goroutine 存在数据错误
for 循环中迭代的变量,都是指向同一个内存地址,即 for 循环中创建的闭包都为引用传递,而由于 goroutine 本身的执行速度,很可能主线程已经循环结束,子线程才开始进行执行,导致数据获取的已经不是想象中的那个值。
package main
import (
"fmt"
"time"
)
func main() {
data := []string{"one","two","three"}
for _,v := range data {
go func() {
fmt.Println(v)
}()
}
time.Sleep(3 * time.Second)
}
/*
three
three
three
*/
然而,若主线程中存在io等待拖慢速度,会发现 子线程仍然会获取到想象中的值。
package main
import (
"fmt"
"time"
)
func main() {
data := []string{"one","two","three"}
for _,v := range data {
go func() {
fmt.Println(v)
}()
time.Sleep(3 * time.Second)
}
time.Sleep(3 * time.Second)
}
/*
one
two
three
*/
解决方式:1、保存每次迭代的值 2、每次迭代的值是匿名 goroutine 的参数
package main
import (
"fmt"
"time"
)
func main() {
data := []string{"one","two","three"}
for _,v := range data {
vcopy := v // 保存每次迭代的值,值传递,仅仅复制值
go func() {
fmt.Println(vcopy)
}()
}
time.Sleep(3 * time.Second)
//goroutines print: one, two, three
}
ackage main
import (
"fmt"
"time"
)
func main() {
data := []string{"one","two","three"}
for _,v := range data {
go func(in string) {
fmt.Println(in)
}(v) // 作为参数传入执行
}
time.Sleep(3 * time.Second)
//goroutines print: one, two, three
}
注意:若循环的对象是指针,则每次取的地址都不同,则不存在指向错误。
package main
import (
"fmt"
"time"
)
type field struct {
name string
}
func (p *field) print() {
fmt.Println(p.name)
}
func main() {
data := []*field{{"one"}, {"two"}, {"three"}}
for _, v := range data {
fmt.Printf("----->%v\n", v)
go v.print()
}
time.Sleep(3 * time.Second)
}
/*
----->&{one}
----->&{two}
----->&{three}
three
two
one
*/
四、Goroutine 阻塞导致的资源泄露
func First(query string, replicas ...Search) Result {
c := make(chan Result)
searchReplica := func(i int) { c <- replicas[i](query) }
for i := range replicas {
go searchReplica(i)
}
return <-c
}
改函数每次循环都会起一个 goroutine,每一个 goroutine 会将结果发送到 channel中。
但由于没有缓存,则只有第一个 goroutine 放入了数据并释放资源,其他的 goroutine 在写入数据前,就已经被中断,导致阻塞。
func First(query string, replicas ...Search) Result {
// 使用相同容量大小的缓存信道,可以有效防止线程阻塞
c := make(chan Result,len(replicas))
searchReplica := func(i int) { c <- replicas[i](query) }
for i := range replicas {
go searchReplica(i)
}
return <-c
}
func First(query string, replicas ...Search) Result {
c := make(chan Result,1)
searchReplica := func(i int) {
// 存在 default 的select 保证了当前 channel 无法收到消息的情况下,也不会阻塞
select {
case c <- replicas[i](query):
default:
}
}
for i := range replicas {
go searchReplica(i)
}
return <-c
}
func First(query string, replicas ...Search) Result {
c := make(chan Result)
// 使用特殊的取消的 channel 终止 workers,worker 无法放入数据
done := make(chan struct{})
defer close(done)
searchReplica := func(i int) {
select {
case c <- replicas[i](query):
case <- done:
}
}
for i := range replicas {
go searchReplica(i)
}
return <-c
}
五、在使用 goroutine 的基础上提高并发、并行量
Go 1.4 及以下版本值使用一个执行上下文、OS线程,这意味着只有一个 goroutine 能在任何时间执行。从1.5版本开始,可以使用 runtime.NumCPU() 用于设置CPU的启动数量,这个数量可以设置到超过实际上系统的CPU数量。你可以使用 GOMAXPROCS 这个环境变量,通过 runtime.GOMAXPROCS() 调整CPU的值
GOMAXPROCS 代表CPU的数量,可以使用该数量来允许 goroutine。且该值的数量可以设置到大于CPU的数量,最大值为 256。
package main
import (
"fmt"
"runtime"
)
func main() {
fmt.Println(runtime.GOMAXPROCS(-1)) //prints: 1
fmt.Println(runtime.NumCPU()) //prints: 1 (on play.golang.org)
runtime.GOMAXPROCS(20)
fmt.Println(runtime.GOMAXPROCS(-1)) //prints: 20
runtime.GOMAXPROCS(300)
fmt.Println(runtime.GOMAXPROCS(-1)) //prints: 256
}
六、无法保证多个 goroutine 的执行顺序
go 会对某些操作进行重新排序,可以保证一个 goroutine 内的执行顺序不变,不能保证多个 goroutine 的执行顺序。
package main
import (
"runtime"
"time"
)
var _ = runtime.GOMAXPROCS(3)
var a, b int
func u1() {
a = 1
b = 2
}
func u2() {
a = 3
b = 4
}
func p() {
println(a)
println(b)
}
func main() {
go u1()
go u2()
go p()
time.Sleep(1 * time.Second)
}
每次运行,打印的结果都不相同。
注意:如果需要保持执行顺序,需要使用 channel 或使用 sync 包构建合适的结构体
七、goroutine 优先调度冲突
如果存在一个不让调度器运行的 for 循环时,就会发生 一个goroutine 阻止其他 goroutine 运行的情况
package main
import "fmt"
func main() {
done := false
go func(){
done = true
}()
for !done {
// 包含不会触发调度器执行的代码,就会导致阻止问题
}
fmt.Println("done!")
}
调度器会在 GC、go 声明、阻塞 channel操作、阻塞系统调用、lock操作、非内联函数调用之后运行
package main
import "fmt"
func main() {
done := false
go func(){
done = true
}()
for !done {
fmt.Println("not done!") //not inlined
}
fmt.Println("done!")
}
// 查看是否为内联函数
go build -gcflags -m
可以使用 runtime.Goshed() 显示唤起调度器
package main
import (
"fmt"
"runtime"
)
func main() {
done := false
go func(){
done = true
}()
for !done {
runtime.Gosched()
}
fmt.Println("done!")
}