学习笔记之go语句的执行规则(观go核心编程36讲)

10 篇文章 0 订阅
9 篇文章 0 订阅

文章目录


前言:

进程,描述的就是程序的执行过程,是运行着的程序的代表。换句话说,一个进程其实就是某个程序运行时的一个产物。如果说静静地躺在那里的代码就是程序的话,那么奔跑着的、正在发挥着既有功能的代码就可以被称为进程。我们的电脑为什么可以同时运行那么多应用程序?我们的手机为什么可以有那么多 App 同时在后台刷新?这都是因为在它们的操作系统之上有多个代表着不同应用程序或 App 的进程在同时运行。

再来说说线程。首先,线程总是在进程之内的,它可以被视为进程中运行着的控制流(或者说代码执行的流程)。一个进程至少会包含一个线程。如果一个进程只包含了一个线程,那么它里面的所有代码都只会被串行地执行。每个进程的第一个线程都会随着该进程的启动而被创建,它们可以被称为其所属进程的主线程。相对应的,如果一个进程中包含了多个线程,那么其中的代码就可以被并发地执行。除了进程的第一个线程之外,其他的线程都是由进程中已存在的线程创建出来的。也就是说,主线程之外的其他线程都只能由代码显式地创建和销毁。这需要我们在编写程序的时候进行手动控制,操作系统以及进程本身并不会帮我们下达这样的指令,它们只会忠实地执行我们的指令。


一、什么是主 goroutine,它与我们启用的其他 goroutine 有什么不同??:

老样子先看代码:


package main

import "fmt"

func main() {
  for i := 0; i < 10; i++ {
    go func() {
      fmt.Print(i)
    }()
  }
}

如果没有了解过go函数对此题结果一定会认为是

0123456789

但实际讷却是这样的


是的啥也没有,那为什么会造成这样的结果?

go函数的执行时间总是会明显滞后于它所属的go语句的执行时间:

go函数真正被执行的时间,总会与其所属的go语句被执行的时间不同。当程序执行到一条go语句的时候,Go 语言的运行时系统,会先试图从某个存放空闲的 G 的队列中获取一个 G(也就是 goroutine),它只有在找不到空闲 G 的情况下才会去创建一个新的 G。
这也说明了为什么总是说去执行一个G而非创建一个G


在拿到了一个空闲的 G 之后,Go 语言运行时系统会用这个 G 去包装当前的那个go函数(或者说该函数中的那些代码),然后再把这个 G 追加到某个存放可运行的 G 的队列中。这类队列中的 G 总是会按照先入先出的顺序,很快地由运行时系统内部的调度器安排运行。虽然这会很快,但是由于上面所说的那些准备工作还是不可避免的,所以耗时还是存在的。

因此,go函数的执行时间总是会明显滞后于它所属的go语句的执行时间
只要go语句本身执行完毕,Go 程序完全不会等待go函数的执行,它会立刻去执行后边的语句。这就是所谓的异步并发地执行。

而对于刚刚那个测试只循环到了10,所以已经迭代结束了但是go func依旧还在准备,那如果是100呢,那go func准备时间就不会超过总的迭代时间了
这样的话当go func准备就绪时迭代的i值是不确定的,这样打印的就是无规则的0-100的数


二、怎样才能让主 goroutine 等待其他 goroutine?

、简单粗暴:让主G sleep就行


for i := 0; i < 10; i++ {
  go func() {
    fmt.Println(i)
  }()
}
time.Sleep(time.Millisecond * 500)

这就存在一个让主G睡多久的问题了
如果“睡眠”太短,则很可能不足以让其他的 goroutine 运行完毕,而若“睡眠”太长则纯属浪费时间,这个时间就太难把握了。


既然不容易预估时间,那我们就让其他的 goroutine 在运行完毕的时候告诉我们好了。
而这个任务自然是交给channel再好不过了


我们先创建一个通道,它的长度应该与我们手动启用的 goroutine 的数量一致。在每个手动启用的 goroutine 即将运行完毕的时候,我们都要向该通道发送一个值。注意,这些发送表达式应该被放在它们的go函数体的最后面。对应的,我们还需要在main函数的最后从通道接收元素值,接收的次数也应该与手动启用的 goroutine 的数量保持一致。


当我们仅仅把通道当作传递某种简单信号的介质的时候,用struct{}作为其元素类型是再好不过的了。
它占用的内存空间是0字节。确切地说,这个值在整个 Go 程序中永远都只会存在一份。虽然我们可以无数次地使用这个值字面量,但是用到的却都是同一个值。

三:怎样让我们启用的多个 goroutine 按照既定的顺序运行?


for i := 0; i < 10; i++ {
  go func(i int) {
    fmt.Println(i)
  }(i)
}

对于这一步使得Go 语言能保证每个 goroutine 都可以拿到一个唯一的整数。其原因与go函数的执行时机有关。
在go语句被执行时,我们传给go函数的参数i会先被求值,如此就得到了当次迭代的序号。之后,无论go函数会在什么时候执行,这个参数值都不会变。也就是说,go函数中调用的fmt.Println函数打印的一定会是那个当次迭代的序号。


接着下一部更改


for i := uint32(0); i < 10; i++ {
  go func(i uint32) {
    fn := func() {
      fmt.Println(i)
    }
    trigger(i, fn)
  }(i)
}


trigger := func(i uint32, fn func()) {
  for {
    if n := atomic.LoadUint32(&count); n == i {
      fn()
      atomic.AddUint32(&count, 1)
      break
    }
    time.Sleep(time.Nanosecond)
  }
}

转载自go核心编程36讲,这一段并未吃透,需要反复咀嚼

trigger函数会不断地获取一个名叫count的变量的值,并判断该值是否与参数i的值相同。如果相同,那么就立即调用fn代表的函数,然后把count变量的值加1,最后显式地退出当前的循环。否则,我们就先让当前的
goroutine“睡眠”一个纳秒再进入下一个迭代。注意,我操作变量count的时候使用的都是原子操作。这是由于trigger函数会被多个
goroutine 并发地调用,所以它用到的非本地变量count,就被多个用户级线程共用了。因此,对它的操作就产生了竞态条件(racecondition),破坏了程序的并发安全性。所以,我们总是应该对这样的操作加以保护,在sync/atomic包中声明了很多用于原子操作的函数。另外,由于我选用的原子操作函数对被操作的数值的类型有约束,所以我才对count以及相关的变量和参数的类型进行了统一的变更(由int变为了uint32)。纵观count变量、trigger函数以及改造后的for语句和go函数,我要做的是,让count变量成为一个信号,它的值总是下一个可以调用打印函数的go函数的序号。这个序号其实就是启用goroutine时,那个当次迭代的序号。也正因为如此,go函数实际的执行顺序才会与go语句的执行顺序完全一致。此外,这里的trigger函数实现了一种自旋(spinning)。除非发现条件已满足,否则它会不断地进行检查。最后要说的是,因为我依然想让主
goroutine 最后一个运行完毕,所以还需要加一行代码。不过既然有了trigger函数,我就没有再使用通道。

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值