学习笔记之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函数,我就没有再使用通道。

相关文章
|
3月前
|
监控 安全 Java
Go语言学习笔记(一)
Go语言学习笔记(一)
107 1
|
4月前
|
安全 Go 数据处理
Go语言CSP编程实战:通道通信技术
Go语言CSP编程实战:通道通信技术
49 0
|
4月前
|
Go
高效Go语言编程:os包实用技术大揭示
高效Go语言编程:os包实用技术大揭示
46 0
|
2月前
|
SQL 前端开发 Go
编程笔记 GOLANG基础 001 为什么要学习Go语言
编程笔记 GOLANG基础 001 为什么要学习Go语言
|
13天前
|
数据库连接 Go 数据库
【Go 语言专栏】Go 语言中的错误注入与防御编程
【4月更文挑战第30天】本文探讨了Go语言中的错误注入和防御编程。错误注入是故意引入错误以测试系统异常情况下的稳定性和容错性,包括模拟网络故障、数据库错误和手动触发错误。防御编程则强调编写代码时考虑并预防错误,确保程序面对异常时稳定运行。Go语言的错误处理机制包括多返回值和自定义错误类型。结合错误注入和防御编程,可以提升软件质量和可靠性,打造更健壮的系统。开发人员应重视这两方面,以实现更优质的软件产品。
|
13天前
|
网络协议 安全 Go
【Go语言专栏】Go语言中的WebSocket编程
【4月更文挑战第30天】本文介绍了在Go语言中使用WebSocket进行实时Web应用开发的方法。通过第三方包`gorilla/websocket`,开发者可建立WebSocket服务器和客户端。文中展示了如何创建服务器,升级HTTP连接,以及处理读写消息的示例代码。同时,客户端的创建和通信过程也得以阐述。文章还提及WebSocket的生命周期管理、性能与安全性考虑,以及实践中的最佳做法。通过学习,读者将能运用Go语言构建高效、实时的Web应用。
|
13天前
|
算法 Java Go
【Go语言专栏】Go语言中的泛型编程探索
【4月更文挑战第30天】Go语言新引入的泛型编程支持使得代码更通用灵活。通过类型参数在函数和接口定义中实现泛型,如示例中的泛型函数`Swap`和泛型接口`Comparator`。泛型应用包括数据结构、算法实现、函数包装和错误处理,提升代码复用与维护性。这一特性扩展了Go语言在云计算、微服务、区块链等领域的应用潜力。
|
18天前
|
SQL 关系型数据库 MySQL
Golang数据库编程详解 | 深入浅出Go语言原生数据库编程
Golang数据库编程详解 | 深入浅出Go语言原生数据库编程
|
19天前
|
网络协议 Linux Go
Go语言TCP Socket编程(下)
Go语言TCP Socket编程
|
19天前
|
网络协议 Ubuntu Unix
Go语言TCP Socket编程(上)
Go语言TCP Socket编程