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

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

前言:

进程,描述的就是程序的执行过程,是运行着的程序的代表。换句话说,一个进程其实就是某个程序运行时的一个产物。如果说静静地躺在那里的代码就是程序的话,那么奔跑着的、正在发挥着既有功能的代码就可以被称为进程。我们的电脑为什么可以同时运行那么多应用程序?我们的手机为什么可以有那么多 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月前
|
数据库连接 Go 数据库
Go语言中的错误注入与防御编程。错误注入通过模拟网络故障、数据库错误等,测试系统稳定性
本文探讨了Go语言中的错误注入与防御编程。错误注入通过模拟网络故障、数据库错误等,测试系统稳定性;防御编程则强调在编码时考虑各种错误情况,确保程序健壮性。文章详细介绍了这两种技术在Go语言中的实现方法及其重要性,旨在提升软件质量和可靠性。
34 1
|
1月前
|
数据采集 监控 Java
go语言编程学习
【11月更文挑战第3天】
42 7
|
1月前
|
Unix Linux Go
go进阶编程:Golang中的文件与文件夹操作指南
本文详细介绍了Golang中文件与文件夹的基本操作,包括读取、写入、创建、删除和遍历等。通过示例代码展示了如何使用`os`和`io/ioutil`包进行文件操作,并强调了错误处理、权限控制和路径问题的重要性。适合初学者和有经验的开发者参考。
|
2月前
|
Java 大数据 Go
Go语言:高效并发的编程新星
【10月更文挑战第21】Go语言:高效并发的编程新星
54 7
|
2月前
|
Go 数据处理 调度
Go语言中的并发模型:解锁高效并行编程的秘诀
本文将探讨Go语言中独特的并发模型及其在现代软件开发中的应用。通过深入分析 Goroutines 和 Channels,我们将揭示这一模型如何简化并行编程,提升应用性能,并改变开发者处理并发任务的方式。不同于传统多线程编程,Go的并发方法以其简洁性和高效性脱颖而出,为开发者提供了一种全新的编程范式。
|
3月前
|
存储 缓存 Go
go语言编程系列(五)
go语言编程系列(五)
|
3月前
|
搜索推荐 Java 编译器
go语言编程系列(四)
go语言编程系列(四)
Go语言的条件控制语句及循环语句的学习笔记
本文是Go语言的条件控制语句和循环语句的学习笔记,涵盖了if语句、if-else语句、if嵌套语句、switch语句、select语句以及for循环和相关循环控制语句的使用方法。
Go语言的条件控制语句及循环语句的学习笔记
|
3月前
|
存储 JSON 安全
go语言编程系列(七)
go语言编程系列(七)
|
3月前
|
存储 安全 编译器
go语言编程系列(六)
go语言编程系列(六)