Golang扫盲式学习——GO并发 | (一)

这篇具有很好参考价值的文章主要介绍了Golang扫盲式学习——GO并发 | (一)。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。

并发与并行😣

并发与并行的概念和区别

并行:同一个时间段内多个任务同时在不同的CPU核心上执行。强调同一时刻多个任务之间的”同时执行“。

并发:同一个时间段内多个任务都在进展。强调多个任务间的”交替执行“。

Golang扫盲式学习——GO并发 | (一)

随着硬件水平的提高,现在的终端主机都是多个CPU,每个CPU都是多核结构。当多个CPU同时运行起来,跑不同的任务,这属于并行;在一个CPU里的多个核心里同时运行不同的任务,同样也属于并行。而并发是关注一个核心里的多个任务,这时需要交替执行,就是并发。

CPU是计算单元,有数据才能进行计算。当一个任务被网络I/O阻塞,CPU没有数据,就会处于等待。显然,若是能够将等待的时间利用起来,资源利用率会提高。因此,并发处理的主要目的是提高CPU和资源的利用率。

Go语言并发与CPU核心的关系

  • Go并发是基于Goroutine和Channel实现的。

Goroutine是Go语言的并发执行单元,Channel用于Goroutine之间的通信与同步。

  • Goroutine的执行依赖于操作系统的线程调度。

Goroutine自身不具备执行上下文,它必须依存在操作系统线程上才可以真正执行。当一个Goroutine被创建时,Go runtime会自动选择一个空闲的操作系统线程,将这个Goroutine的执行上下文绑定到该线程上。

Golang扫盲式学习——GO并发 | (一)

上图中G表示Goroutine,P表示一个调度的上下文(包含了运行 Goroutine 的资源),M表示一个OS线程。一个操作系统线程可以同时关联多个Goroutine,这些Goroutine会被Go runtime高效地在该线程上调度执行。但任意时刻只会有一个Goroutine获得线程的执行权进行运行(G0就是获得线程权的Goroutine)。当关联的操作系统线程终止时,绑定在该线程上的所有Goroutine也会被终止。

  • 在单核CPU上,即使有许多Goroutine,同一时刻也只能有一个Goroutine真正在CPU上运行。

单核CPU同一时刻只能执行一个线程。即使有许多Goroutine,也只有获得CPU执行权的那个Goroutine在真正运行,其他Goroutine会被挂起,等待下次被调度执行。

  • 在多核CPU上,操作系统可以将不同的Goroutine直接调度到不同的CPU核心上运行。

这样多个Goroutine就可以同时真正运行,实现并行执行。此时Go并发程序可以发挥多核CPU的强大计算能力。Go runtime会有智能的调度策略,将Goroutine均匀地分布在所有CPU核心上或者以负载均衡的方式进行调度,这取决于Goroutine的数量和系统的CPU核心数。

Golang扫盲式学习——GO并发 | (一)

GMP模型如上图所示。图中涉及5个重要的实体。

  • 全局队列(run queue):存放等待运行的Goroutine。
  • 本地队列(local queue):和P连接的队列,存放的也是等待运行的Goroutine,存放数量有限,一般不超过256个。新建Goroutine时,优先在本地队列存放,然后再放置全局队列。
  • P:被称为处理器,包含了运行 Goroutine 的资源。一个Goroutine想要运行,必须先获取P。P的数量是可配置的,最多有GOMAXPROCS个。
  • G:指代Goroutine,会被Go runtime智能化调度。当一个线程M空闲时,首先会从全局队列里获取Goroutine,若全局队列为空,则会从周边的线程”偷“一半Goroutine放到本地队列。
  • M:线程由OS调度器分配到CPU的核上执行。当一个线程阻塞时,会导致和该线程的其它Goroutine”饿死“。此时,Go runtime会解绑P和M,将一个新的M分配给P避免”饿死“情况发生。

Goroutine与OS线程的区别

  • 生命周期不同

Goroutine由Go runtime 管理生命周期,创建和销毁由runtime调度完成。OS线程是由操作系统内核来管理生命周期。

  • 调度不同

Goroutine由Go runtime的调度器进行调度。OS线程的调度是由操作系统内核根据时间片进行的。

  • 关联关系不同

每个OS线程与一个Goroutine关联,但一个Goroutine不一定对应一个OS线程,多个Goroutine可能对应同一个OS线程。Go runtime会动态地将Goroutine映射到线程上。

  • 资源消耗不同

创建和维护OS线程需要较多资源,而Goroutine的资源消耗很小。一个应用程序可以同时存在成千上万个Goroutine,但OS线程数目通常较小。

  • 通信方式不同

Goroutine之间通信使用Channel,而OS线程通常使用共享内存来通信。

  • 并发数不同

一个Go程序的并发度可以达到上百万,这是由于Goroutine的高效率实现。OS线程难以达到如此高的并发度。

Goroutine🤔

Goroutine的概念与特点

概念:Goroutine是一个轻量级的执行单元,用于执行并发任务。多个Goroutine可以在同一地址空间中执行,且Go runtime会管理其生命周期。Goroutine通过Channel进行通信。

特点:

  • 轻量级:创建和维护Goroutine的开销很小,一个程序可以同时存在成千上万个Goroutine。
  • 并发执行:Goroutine允许程序利用多核CPU的优势进行并发执行。
  • 自动调度:Goroutine的调度完全由Go运行时进行管理,开发者不需要关心底层细节。
  • 资源共享:多个Goroutine可以访问共享的内存资源,这使得Goroutine之间可以高效地通信与协作。
  • 无需回收:Goroutine不需要手动回收即可释放资源,运行时会自动回收结束的Goroutine。
  • 动态扩展:一个Go程序可以从几个Goroutine开始,然后动态地创建更多Goroutine来利用多核资源。
  • 高并发:利用Goroutine可以轻易地编写高并发程序,一个服务器程序可以同时接待成千上万个客户端。
  • Channel通信:Goroutine之间可以通过Channel进行高效的消息通信与同步,这使得编写并发程序变得简单。

创建Goroutine的语法

go func(){}():第一个func() {...} 定义了一个匿名函数(anonymous function)。第二个()代表调用这个匿名函数。

package main

import (
	"fmt"
	"time"
)

func main() {
	// 创建一个Goroutine
	go func() {
		fmt.Println("Hello from Goroutine!")
	}()

	time.Sleep(1)
	// 主Goroutine
	fmt.Println("Hello from main!")
}

Goroutine的调度与上下文切换

为了让不同的Goroutine有机会运行,runtime会在Goroutine之间进行上下文切换。当一个Goroutine运行一定时间或遇到channel操作时,会主动交出线程的执行权,这时runtime会从其他挂起的Goroutine中选择一个继续运行。上下文切换涉及到保存当前运行Goroutine的程序计数器、堆栈指针等上下文信息,并恢复下一个要运行的Goroutine的上下文信息,这个过程需要一定的时间开销。

Goroutine存在的内存问题及解决方案

存在的问题:

  • 栈溢出:每个Goroutine都有一个私有的栈,默认栈大小为2MB,如果函数调用太深会导致栈溢出。

  • 堆溢出:如果Goroutine中分配过多的堆对象,也会导致内存溢出。

  • 内存泄露:如果Goroutine退出时没有释放之前分配的内存,会导致这部分内存泄漏。

解决方案:

  • Goroutine栈大小

方法一:设置runtime.GOMAXPROCS(n, stackSize),其中,n指定要使用的P的个数,设置为0表示使用所有CPU核心,stackSize指定新的默认Goroutine栈大小,单位为字节。

方法二:创建Goroutine时,指定栈的大小。go func(params) { /* ... */ }(stackSize, params),stackSize必须是第一个参数,在params之前指定。注意:stackSize的参数只在编译时起作用,用于指定Goroutine的栈大小,在被调用的函数内部,它无法访问这个参数。

  • 避免无限递归

在Goroutine中调用无限递归函数会引起栈溢出,应该避免这种情况发生。

  • 设置垃圾回收阈值

可以通过runtime.GOGC来设置垃圾回收器的阈值,触发更频繁的垃圾回收来避免堆溢出,例如runtime.GOGC=200 设置垃圾回收阈值为200。

  • 手动回收堆内存

当一个Goroutine结束时,其私有栈内存会被收回,但堆内存不会自动回收。因此,需要在Goroutine结束前手动回收不再使用的堆内存,否在会发生内存泄露。

go func() {
    // 分配一些堆空间
    buf := make([]byte, 100)
    // 使用buf...
   
    // Goroutine结束前手动回收buf
    buf = nil 
}()
var wg sync.WaitGroup
wg.Add(1)
go func() {
    defer wg.Done()   // Goroutine结束 decrement WaitGroup计数
    // 分配内存并使用...
    buf := make([]byte, 100) 
    // 使用buf...
    buf = nil        // 手动回收内存
}()
wg.Wait()           // 等待Goroutine结束

Channel👍

Channel的概念与特点

Channels are not closed by default. They need to be closed explicitly with the Close method to indicate that no more values will be sent on the channel.

Channel是一个通信机制,它可以使多个Goroutine之间相互发送数据。Channel允许任意两个Goroutine通过它异步地传递信息。

Golang扫盲式学习——GO并发 | (一)

特点:

  • 方向性:Channel可以是双向的(default)或单向的(指定方向时)。单向Channel按发送/接收方向分为发送(chan<-)和接收(<-chan)Channel。

  • 类型安全:Channel在声明时需指定元素类型,之后只能传送该类型的元素。这保证了Channel通信的类型安全。

  • FIFO:Channel实现了先入先出的规则,发送的元素按顺序被接收。

  • 阻塞:向一个满的Channel发送数据会导致发送方阻塞,从一个空的Channel接收数据会导致接收方阻塞。

  • 缓冲:可以指定Channel的缓冲区大小,向一个未满的缓冲Channel发送数据不会阻塞。

  • 关闭:关闭的Channel无法再发送数据,但可以继续从中接收数据。向关闭的Channel发送数据会panic。

  • 无缓冲或满的Channel导致Goroutine阻塞,这可用于实现同步和协作。

  • Channel支持for range形式的接收,这会不断接收Channel的数据知道它被关闭。

  • Channel可用于函数间传递数据,实现异步执行的函数之间的数据通信。

无缓冲Channel与有缓冲Channel

  • 无缓冲Channel

unbuffered channel 就是缓冲大小为 0 的 channel,无缓冲区的 channel 本身是不存放数据的,在发送和接收都会被阻塞。也就是相当于,你现在是一个 send 身份,但是当另外一个没有 receive 你发送的值之前,你一直处于阻塞(等待接收)状态;就好比你递东西给别人,别人没接,你就要一直举着东西。相反,如果你现在是一个 receive 身份,你就会一直阻塞(等待发送)状态,在你拿到值之前,你会一直等待。就好比你准备要接东西,别人迟迟不给你,你就要一直等着。

package main

import (
	"fmt"
)

func main() {
	ch := make(chan struct{})
	go func() {
		defer close(ch)
		v := <-ch
		fmt.Printf("receive a struct: %v\n", v)
	}()

	ch <- struct{}{}
	fmt.Println("send a struct")
}
  • 有缓冲Channel

Golang扫盲式学习——GO并发 | (一)

有缓冲的Channel就是设置了一个buffersize,作为缓冲区的大小。

package main

import (
	"fmt"
	"time"
)

func main() {
	ch := make(chan int, 1)  // 设置buffersize大小为1
	go func() {
		defer close(ch)
		defer fmt.Println("我关闭了")
		ch <- 1
		ch <- 2
		fmt.Println("send a struct")
	}()

	time.Sleep(5 * time.Second)
	for {
		v, ok := <-ch
		if !ok {
			fmt.Println("channel closed")
			break
		}
		fmt.Printf("received a struct %v\n", v)
	}
}
// output
received a struct 1
received a struct 2
send a struct
我关闭了
channel closed

创建Channel时,当buffersize==1时,相当于buffersize==0,表示创建一个无缓冲的通道。所以,当向通道传入一个1时,缓冲区就满了。此时,必须等接收端将1读走,才能继续向channel发送2。

将buffersize修改为2时,表示创建一个有缓冲的通道。发送者会一次性将1和2发送到通道里,然后就关闭Channel的写入端。睡眠5秒后,就由接收端将结果读出。

// output
send a struct
我关闭了
received a struct 1
received a struct 2
channel closed

应用:实现一个生产者消费者模型

package main

import (
	"fmt"
	"time"
)

func producer(out chan<- int) {
	for i := 0; i < 10; i++ {
		out <- i  // 生产一个数据,发送到Channel
		fmt.Println("生产者:", i)
	}
	close(out)
}

func consumer(in <-chan int) {
	for num := range in {  // 从Channel中接收数据
		fmt.Println("消费者:", num)  // 消费数据
	}
}

func main() {
	// 创建一个管道
	ch := make(chan int)

	go producer(ch)  // 启动生产者Goroutine
	go consumer(ch)  // 启动消费者Goroutine
	go consumer(ch)  // 启动消费者Goroutine

	time.Sleep(5 * time.Second)  // 等待
}

既然都看到这里了,不如顺手点个推荐吧!
Golang扫盲式学习——GO并发 | (一)

参考资料:

https://morsmachine.dk/go-scheduler

https://go.dev/blog/pipelines

https://www.kelche.co/blog/go/golang-scheduling/

https://www.kelche.co/blog/go/channels/

https://juejin.cn/post/7231887884739346489文章来源地址https://www.toymoban.com/news/detail-465537.html

到了这里,关于Golang扫盲式学习——GO并发 | (一)的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • 是时候回答【我为什么要学习 Go 语言(golang)】这个问题了

    想必每个人在学习新事物之前,都会扪心自问:“我为什么要学习它呢?” 正如我们读 四大名著 一般,也只有在您读过了 四大名著 后,再细看中国几千年历史不就是 天下大势合久必分,分久必合 ,再者,便是与友数人相聚,席间您述说您通勤时所遇到有意思的事了,而您

    2023年04月09日
    浏览(44)
  • 100天精通Golang(基础入门篇)——第5天: Go语言中的数据类型学习

    🌷 博主 libin9iOak带您 Go to Golang Language.✨ 🦄 个人主页——libin9iOak的博客🎐 🐳 《面试题大全》 文章图文并茂🦕生动形象🦖简单易学!欢迎大家来踩踩~🌺 🌊 《IDEA开发秘籍》学会IDEA常用操作,工作效率翻倍~💐 🪁 希望本文能够给您带来一定的帮助🌸文章粗浅,敬请批

    2024年02月08日
    浏览(39)
  • 【Go学习】macOS+IDEA运行golang项目,报command-line-arguments,undefined

    1、写在前面的话:idea如何配置golang,自行百度 2、送福利:国内好用的ChatGpt有很多,比如:天工、文心一言、讯飞星火、通义万相等 问题1:通过idea的terminal执行go test报错 这个问题就是当前目录没有go.mod文件,直接用go命令生成一个即可(example.com/m 可以随便自定义,比如:

    2024年01月18日
    浏览(42)
  • 分享一个项目:`learning_go_plan9_assembly`, 学习 golang plan9 汇编

    作者:张富春(ahfuzhang),转载时请注明作者和引用链接,谢谢! cnblogs博客 zhihu Github 公众号:一本正经的瞎扯 近期在学习 golang plan9 汇编,总算基本做到了手写汇编,并整理了很多笔记。 plan9 汇编的资料少,难学,难用。可能也有想学习汇编的人会遇到与我一样的问题。 于是把

    2024年02月06日
    浏览(43)
  • golang并发安全-select

    前面说了golang的channel, 今天我们看看golang select 是怎么实现的。 select 非默认的case 中都是处理channel 的 接受和发送,所有scase 结构体中c是用来存储select 的case中使用的channel 编译器在中间代码生成期间会根据 select 中 case 的不同对控制语句进行优化,这一过程都发生在cmd/c

    2024年01月23日
    浏览(40)
  • Golang 并发编程详解

    并发是现代软件开发中的一个重要概念,它允许程序同时执行多个任务,提高系统的性能和响应能力。Golang 是一门天生支持并发的语言,它通过 goroutine 和 channel 提供了强大的并发编程支持。 在本文中,我们将深入探讨 Golang 中的并发编程,了解 goroutine、channel 以及一些常见

    2024年01月23日
    浏览(39)
  • golang 并发

    Golang 并发 并行 指的是在同一时刻 有多条指令在多个CPU处理器上同时执行 2个任务2个窗口需要硬件支持 并发是指在同一时刻 只能有一条指令 单多个进程指令快速轮换执行 2个队伍1个窗口 要求提升软件能力 Golang 并发优势 go 从底层就支持并发 简化了并发程序的编写方法 Go

    2024年02月03日
    浏览(31)
  • Golang 并发 Channel的用法

    上面是创建了无缓冲的 channel,一旦有 goroutine 往 channel 发送数据,那么当前的 goroutine 会被阻塞住,直到有其他的 goroutine 消费了 channel 里的数据,才能继续运行。 上面示例中的第二个参数表示 channel 可缓冲数据的容量。只要当前 channel 里的元素总数不大于这个可缓冲容量,

    2024年02月21日
    浏览(42)
  • Golang并发控制方式有几种?

    Go语言中的goroutine是一种轻量级的线程,其优点在于占用资源少、切换成本低,能够高效地实现并发操作。但如何对这些并发的goroutine进行控制呢? 一提到并发控制,大家最先想到到的是锁。Go中同样提供了锁的相关机制,包括互斥锁 sync.Mutex 和读写锁 sync.RWMutex ;除此之外

    2024年02月19日
    浏览(31)
  • Golang之路---04 并发编程——WaitGroup

    为了保证 main goroutine 在所有的 goroutine 都执行完毕后再退出,前面使用了 time.Sleep 这种简单的方式。 由于写的 demo 都是比较简单的, sleep 个 1 秒,我们主观上认为是够用的。 但在实际开发中,开发人员是无法预知,所有的 goroutine 需要多长的时间才能执行完毕,sleep 多了,

    2024年02月14日
    浏览(26)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包