Go - Goroutine

一、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!")
}

参考阅读

Go 语言与设计 调度器

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值