当前位置: 首页 > article >正文

go语言的成神之路-筑基篇-并发

目录

一、go协程

 二、GMP

示例:

GMP 的工作流程:

使用 GMP 的好处:

注意事项:

三、runtime包

Gosched

Goexit

调用Goexit之前 

调用Goexit之前运行的结果 

调用Goexit之后 

调用Goexit之后运行的结果  

 GOMAXPROCS

执行时间不同的原因:

思考:更改参数n的值会使执行时间相同吗?

修改方案

部分代码解析 


一、go协程

在 Go 语言中,使用 goroutine 来实现并发。goroutine 是一种轻量级的线程,可以通过 go 关键字来启动。

package main

import (
	"fmt"
	"time"
)

func hello() {
	fmt.Println("hello goroutine")
}
func main() {
	go hello()
	fmt.Println("main goroutine")
	time.Sleep(time.Second)
}
  • func main() {...}:这是 Go 程序的入口函数,程序从这里开始执行。
  • go hello():使用 go 关键字启动一个新的 goroutine 来调用 hello 函数。goroutine 是 Go 语言中的轻量级线程,它允许并发执行代码。当执行到这一行时,会创建一个新的执行流,该执行流将调用 hello 函数,而不会阻塞当前的执行流程。
  • fmt.Println("main goroutine"):在主 goroutine 中打印出 "main goroutine" 字符串。
  • time.Sleep(time.Second):让主 goroutine 暂停执行一秒钟。这是为了确保程序不会立即退出,因为新启动的 goroutine 可能还没有机会执行完 hello 函数。如果没有这行代码,程序可能会在 hello 函数执行之前就结束,因为主 goroutine 会直接结束,导致整个程序终止。
package main

import (
	"fmt"
	"time"
)

func hello(i int) {
	fmt.Println("hello goroutine", i)
}
func main() {
	for i := 0; i < 10; i++ {
		go hello(i)
	}
	fmt.Println("main goroutine")
	time.Sleep(time.Second)
}

 

仔细观察一下不难看出,这两次输出的结果都不相同,原因如下:

在 Go 语言中,goroutine 的执行顺序是不确定的。当你使用 go 关键字启动多个 goroutine 时,它们会被调度到 Go 的运行时环境中并发执行。

以下是导致打印顺序不同的原因:

  • 调度器的随机性:Go 的调度器会根据系统资源和其他因素来决定 goroutine 的执行顺序。不同的 goroutine 可能在不同的系统线程上执行,并且它们的执行顺序取决于调度器的决策,而不是它们被创建的顺序。
  • 并发执行:由于 goroutine 是并发执行的,它们可能在不同的时间点开始和完成,这取决于系统的负载、CPU 核心的可用性等因素。

思考题 

 二、GMP

  • G (Goroutines):Goroutines 是 Go 语言中的轻量级线程,它们是并发执行的基本单元。可以使用 go 关键字启动一个新的 Goroutine,例如 go func() {...}()。Goroutines 非常轻量,可以轻松创建大量的 Goroutines,并且它们由 Go 的运行时系统管理。
  • M (Workers):M 代表工作线程,它们是操作系统线程,由 Go 的运行时系统创建和管理。M 负责执行 Goroutines,一个 M 可以执行多个 Goroutines。
  • P (Processors):P 是逻辑处理器,它是一个抽象的概念,代表执行上下文。每个 M 都需要绑定到一个 P 上才能执行 Goroutines。P 维护一个本地的 Goroutine 队列,当一个 Goroutine 被创建时,它会被放入一个 P 的队列中等待执行。

示例:

package main

import (
	"fmt"
	"sync"
	"time"
)

func worker(id int, wg *sync.WaitGroup) {
	defer wg.Done()
	fmt.Printf("Worker %d starting\n", id)
	time.Sleep(time.Second)
	fmt.Printf("Worker %d done\n", id)
}

func main() {
	var wg sync.WaitGroup
	// 启动多个 goroutines
	for i := 1; i <= 5; i++ {
		wg.Add(1)
		go worker(i, &wg)
	}
	// 等待所有 goroutines to complete
	wg.Wait()
}
  • var wg sync.WaitGroup:创建一个 sync.WaitGroup 实例,用于等待一组 Goroutines 完成。
  • wg.Add(1):在启动每个 Goroutine 之前,调用 wg.Add(1) 来增加等待组的计数,表示有一个新的 Goroutine 要等待。
  • go worker(i, &wg):启动一个新的 Goroutine 来执行 worker 函数,并将 wg 的指针传递给它。
  • defer wg.Done():在 worker 函数中,使用 defer wg.Done() 来通知 WaitGroup 该 Goroutine 已经完成。
  • wg.Wait():在主函数中,调用 wg.Wait() 会阻塞,直到 WaitGroup 的计数为 0,即所有 Goroutines 都已完成。

GMP 的工作流程

  1. 当你启动一个新的 Goroutine 时,它会被放入一个 P 的本地队列中。
  2. M 从 P 的队列中取出 Goroutines 并执行它们。
  3. 如果一个 P 的队列是空的,M 可以从全局队列中获取 Goroutines 或者从其他 P 的队列中窃取 Goroutines(work stealing)。
  4. 当一个 Goroutine 执行 I/O 操作或阻塞时,M 可以切换到另一个 Goroutine 执行,提高并发性能。

使用 GMP 的好处

  • 高效的并发:Go 的 GMP 模型允许高效地创建和管理大量的并发任务,而不会像传统线程那样消耗大量的系统资源。
  • 自动调度:Go 的运行时系统自动调度 Goroutines,根据系统资源和任务的需求进行优化,提高性能。

注意事项

  • 避免 Goroutine 泄漏:确保 Goroutines 最终会结束,否则可能会导致资源泄漏。
  • 避免过度创建 Goroutines:虽然 Goroutines 很轻量,但创建过多可能会导致性能问题,尤其是在资源有限的系统上。

三、runtime包

Gosched

package main

import (
	"fmt"
	"runtime"
)

func main() {
	// 启动一个匿名 goroutine,接收一个字符串参数 s
	go func(s string) {
		// 循环两次
		for i := 0; i < 2; i++ {
			// 打印传入的字符串 s
			fmt.Println(s)
		}
	}("hello")

	// 主 goroutine
	for i := 0; i < 2; i++ {
		// 让出时间片,让其他 goroutine 有机会执行
		runtime.Gosched()
		// 打印主 goroutine 信息
		fmt.Println("main goroutine")
	}
}

runtime.Gosched():这个函数调用会使当前 goroutine(在这种情况下是主 goroutine)让出处理器,允许其他 goroutine 运行。它放弃自己的时间片并重新进入可运行的 goroutine 队列,以便其他 goroutine 有机会执行。

Goexit

  • runtime.Goexit() 是 Go 语言 runtime 包中的一个函数,它的主要作用是立即终止当前正在执行的 goroutine,而不会影响其他 goroutine 的执行。
  • 当 runtime.Goexit() 被调用时,它会执行当前 goroutine 中已注册的 defer 函数,然后终止该 goroutine,但不会执行 goroutine 中 runtime.Goexit() 之后的代码。

调用Goexit之前 

调用Goexit之前运行的结果 

调用Goexit之后 

调用Goexit之后运行的结果  

 GOMAXPROCS

  • runtime.GOMAXPROCS(n int) 是 Go 语言 runtime 包中的一个函数,它用于设置可以同时执行的最大 CPU 核心数,也就是可以同时运行的 goroutine 的最大数量。
  • 参数 n 表示要使用的 CPU 核心数。如果 n 小于 1,则表示不限制 CPU 核心数,Go 运行时会根据系统的 CPU 核心数自动调整。

可以看出当参数为1的时候执行的时间不同 ,那么原因是什么?

首先对代码部分进行简单分析

package main

import (
	"fmt"
	"runtime"
	"time"
)

func a() {
	for i := 0; i < 5; i++ {
		fmt.Println("A: ", i)
	}
	fmt.Println(time.Now())
}
func b() {
	for i := 0; i < 5; i++ {
		fmt.Println("B: ", i)
	}
	fmt.Println(time.Now())
}
func main() {
	runtime.GOMAXPROCS(1)
	go a()
	go b()
	time.Sleep(time.Second)
}
  • runtime.GOMAXPROCS(1):将最大的可同时执行的 goroutine 数量设置为 1。这意味着在任何给定的时间,只有一个 goroutine 会在 CPU 上执行,其他 goroutine 会等待。
  • go a() 和 go b():启动两个 goroutine,分别执行函数 a 和 b
  • time.Sleep(time.Second):让主 goroutine 暂停执行一秒钟,以等待 a 和 b 两个 goroutine 有机会执行。

执行时间不同的原因:

  • 尽管 runtime.GOMAXPROCS(1) 限制了并发执行的 goroutine 数量为 1,但 goroutine 的调度顺序仍然是不确定的。
  • 当 a 和 b 两个 goroutine 被创建后,它们会被放入 goroutine 队列中等待调度。由于 goroutine 的调度是由 Go 的运行时系统决定的,所以 a 和 b 哪个先被执行是不确定的。
  • 即使 a 先被调度,它可能会在执行过程中被暂停,然后 b 被调度,或者反之。这取决于 Go 运行时的调度决策,包括当前系统的负载、其他 goroutine 的状态等因素。

思考:更改参数n的值会使执行时间相同吗?

  • runtime.GOMAXPROCS(2) 会允许最多 2 个 goroutine 同时运行,但这并不保证 a 和 b 函数的执行顺序会固定。
  • 即使有 2 个 CPU 核心可用,Go 的运行时调度器仍然会根据各种因素(如系统负载、其他 goroutine 的状态等)来决定哪个 goroutine 先执行,以及它们的执行顺序。
  • 每个 goroutine 的执行时间可能会受到系统资源分配、操作系统调度、其他进程的影响,因此即使有更多的 CPU 资源,也不能保证 a 和 b 的执行顺序和时间是固定的。

修改方案

可以通过管道来同步时间

package main

import (
	"fmt"
	"time"
)

func a(ch chan bool) {
	for i := 0; i < 5; i++ {
		fmt.Println("A: ", i)
	}
	fmt.Println(time.Now())
	ch <- true
}

func b(ch chan bool) {
	<-ch
	for i := 0; i < 5; i++ {
		fmt.Println("B: ", i)
	}
	fmt.Println(time.Now())
}

func main() {
	ch := make(chan bool)
	go a(ch)
	go b(ch)
	time.Sleep(time.Second)
}

部分代码解析 

  • ch := make(chan bool):创建一个布尔类型的通道。
  • func a(ch chan bool):函数 a 执行完后会向通道 ch 发送一个 true 值。
  • func b(ch chan bool):函数 b 会从通道 ch 接收一个值,这会阻塞 b 的执行,直到 a 发送一个值到通道。这样可以确保 b 在 a 完成后开始执行。

http://www.kler.cn/a/421916.html

相关文章:

  • opencv常用图像处理操作
  • WordPress ElementorPageBuilder插件 任意文件读取漏洞复现(CVE-2024-9935)
  • linux磁盘管理
  • Redis设计与实现读书笔记
  • Keil5配色方案修改为类似VSCode配色
  • react 使用状态管理调用列表接口渲染列表(包含条件查询,统一使用查询按钮,重置功能),避免重复多次调用接口的方法
  • 亚马逊云(AWS)使用root用户登录
  • MySQL算法篇(一)
  • html button 按钮单选且 高亮
  • web安全之信息收集
  • jupyter+云服务器+内网穿透=无痛远程jupyter服务
  • C++ 分治
  • 基于Matlab高速动车组转臂定位橡胶节点刚度对车辆动力学影响仿真研究
  • 限定符使用
  • 使用 GORM 与 MySQL 数据库进行交互来实现增删改查(CRUD)操作
  • PHP语法学习(第三天)
  • Kali Linux使用Netdiscover工具的详细教程
  • cdn服务
  • java 将本地的音频文件转为流,并把流地址发送到前端,通过浏览器可以播放音频
  • SpringBoot引入mongdb
  • CLIP模型也能处理点云信息
  • redis都有哪些用法
  • PTA--数据结构预习报告:旅游规划问题
  • 计算几何学习,第一天
  • JAVA设计模式,责任链模式
  • MySQL删除数据要谨慎