深入理解Go语言与并发编程底层原理

并发编程,可以说一直都是开发者们关注最多的主题之一。而 Golang 作为一个出道就自带“高并发”光环的编程语言,其并发编程的实现原理肯定是值得我们深入探究的。

Go 并发编程模型在底层是由操作系统所提供的线程库支撑的,这里先简要介绍一下线程实现模型的相关概念。

线程的实现模型

线程的实现模型主要有 3 个,分别是:用户级线程模型、内核级线程模型和两级线程模型。它们之间最大的差异在于用户线程与内核调度实体(KSE)之间的对应关系上。内核调度实体就是可以被操作系统内核调度器调度的对象,也称为内核级线程,是操作系统内核的最小调度单元。

用户级线程模型

用户线程与 KSE 为多对一(N:1)的映射关系。此模型下的线程由用户级别的线程库全权管理,线程库存储在进程的用户空间之中,这些线程的存在对于内核来说是无法感知的,所以这些线程也不是内核调度器调度的对象。一个进程中所有创建的线程都只和同一个 KSE 在运行时动态绑定,内核的所有调度都是基于用户进程的。

对于线程的调度则是在用户层面完成的,相较于内核调度不需要让 CPU 在用户态和内核态之间切换,这种实现方式相比内核级线程模型可以做的很轻量级,对系统资源的消耗会小很多,上下文切换所花费的代价也会小得多。许多语言实现的协程库基本上都属于这种方式。但是,此模型下的多线程并不能真正的并发运行。例如,如果某个线程在 I/O 操作过程中被阻塞,那么其所属进程内的所有线程都被阻塞,整个进程将被挂起。

内核级线程模型

用户线程与 KSE 为一对一(1:1)的映射关系。此模型下的线程由内核负责管理,应用程序对线程的创建、终止和同步都必须通过内核提供的系统调用来完成,内核可以分别对每一个线程进行调度。所以,一对一线程模型可以真正的实现线程的并发运行,大部分语言实现的线程库基本上都属于这种方式。但是,此模型下线程的创建、切换和同步都需要花费更多的内核资源和时间,如果一个进程包含了大量的线程,那么它会给内核的调度器造成非常大的负担,甚至会影响到操作系统的整体性能。

两级线程模型

用户线程与 KSE 为多对多(N:M)的映射关系。两级线程模型吸收前两种线程模型的优点并且尽量规避了它们的缺点,区别于用户级线程模型,两级线程模型中的进程可以与多个内核线程 KSE 关联,也就是说一个进程内的多个线程可以分别绑定一个自己的 KSE,这点和内核级线程模型相似;其次,又区别于内核级线程模型,它的进程里的线程并不与 KSE 唯一绑定,而是可以多个用户线程映射到同一个 KSE,当某个 KSE 因为其绑定的线程的阻塞操作被内核调度出 CPU 时,其关联的进程中其余用户线程可以重新与其他 KSE 绑定运行。所以,两级线程模型既不是用户级线程模型那种完全靠自己调度的也不是内核级线程模型完全靠操作系统调度的,而是一种自身调度与系统调度协同工作的中间态,即用户调度器实现用户线程到 KSE 的调度,内核调度器实现 KSE 到 CPU 上的调度。

Go 的并发机制

在 Go 的并发编程模型中,不受操作系统内核管理的独立控制流不叫用户线程或线程,而称为 Goroutine。Goroutine 通常被认为是协程的 Go 实现,实际上 Goroutine 并不是传统意义上的协程,传统的协程库属于用户级线程模型,而 Goroutine 结合 Go 调度器的底层实现上属于两级线程模型。

Go 搭建了一个特有的两级线程模型。由 Go 调度器实现 Goroutine 到 KSE 的调度,由内核调度器实现 KSE 到 CPU 上的调度。Go 的调度器使用 G、M、P 三个结构体来实现 Goroutine 的调度,也称之为GMP 模型。

GMP 模型

G:表示 Goroutine。每个 Goroutine 对应一个 G 结构体,G 存储 Goroutine 的运行堆栈、状态以及任务函数,可重用。当 Goroutine 被调离 CPU 时,调度器代码负责把 CPU 寄存器的值保存在 G 对象的成员变量之中,当 Goroutine 被调度起来运行时,调度器代码又负责把 G 对象的成员变量所保存的寄存器的值恢复到 CPU 的寄存器

M:OS 底层线程的抽象,它本身就与一个内核线程进行绑定,每个工作线程都有唯一的一个 M 结构体的实例对象与之对应,它代表着真正执行计算的资源,由操作系统的调度器调度和管理。M 结构体对象除了记录着工作线程的诸如栈的起止位置、当前正在执行的 Goroutine 以及是否空闲等等状态信息之外,还通过指针维持着与 P 结构体的实例对象之间的绑定关系

P:表示逻辑处理器。对 G 来说,P 相当于 CPU 核,G 只有绑定到 P(在 P 的 local runq 中)才能被调度。对 M 来说,P 提供了相关的执行环境(Context),如内存分配状态(mcache),任务队列(G)等。它维护一个局部 Goroutine 可运行 G 队列,工作线程优先使用自己的局部运行队列,只有必要时才会去访问全局运行队列,这可以大大减少锁冲突,提高工作线程的并发性,并且可以良好的运用程序的局部性原理

一个 G 的执行需要 P 和 M 的支持。一个 M 在与一个 P 关联之后,就形成了一个有效的 G 运行环境(内核线程+上下文)。每个 P 都包含一个可运行的 G 的队列(runq)。该队列中的 G 会被依次传递给与本地 P 关联的 M,并获得运行时机。

M 与 KSE 之间总是一一对应的关系,一个 M 仅能代表一个内核线程。M 与 KSE 之间的关联非常稳固,一个 M 在其生命周期内,会且仅会与一个 KSE 产生关联,而 M 与 P、P 与 G 之间的关联都是可变的,M 与 P 也是一对一的关系,P 与 G 则是一对多的关系。

G

运行时,G 在调度器中的地位与线程在操作系统中差不多,但是它占用了更小的内存空间,也降低了上下文切换的开销。它是 Go 语言在用户态提供的线程,作为一种粒度更细的资源调度单元,使用得当,能够在高并发的场景下更高效地利用机器的 CPU。

g 结构体部分源码(src/runtime/runtime2.go):

type g struct {
stack stack // Goroutine的栈内存范围[stack.lo, stack.hi)
stackguard0 uintptr // 用于调度器抢占式调度
m *m // Goroutine占用的线程
sched gobuf // Goroutine的调度相关数据
atomicstatus uint32 // Goroutine的状态
...
}

type gobuf struct {
sp uintptr // 栈指针
pc uintptr // 程序计数器
g guintptr // gobuf对应的Goroutine
ret sys.Uintewg // 系统调用的返回值
...
}

gobuf 中保存的内容会在调度器保存或恢复上下文时使用,其中栈指针和程序计数器会用来存储或恢复寄存器中的值,改变程序即将执行的代码。

atomicstatus 字段存储了当前 Goroutine 的状态,Goroutine 主要可能处于以下几种状态:

 

  • 0
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
### 回答1: 学习Go语言并发编程,需要掌握以下几个方面: 1. 学习Goroutine:Goroutine是Go语言并发编程的核心,它是一种轻量级的线程,可以在程序中创建多个Goroutine并发执行,以实现高效的并发编程。需要学习如何创建和管理Goroutine。 2. 学习Channel:Channel是Goroutine之间进行通信的重要手段,可以用于数据传输和同步等操作。需要学习如何创建和使用Channel。 3. 学习Select语句:Select语句是Goroutine之间进行多路复用的重要语法,可以同时监听多个Channel,从而实现高效的并发处理。需要学习如何使用Select语句。 4. 学习Mutex和WaitGroup:Mutex和WaitGroup是Go语言中实现同步和互斥的重要机制,可以用于保护共享资源和协调Goroutine的执行。需要学习如何使用Mutex和WaitGroup。 5. 学习并发编程的设计模式:并发编程的设计模式是一些常用的并发编程思想和模式,可以用于解决并发编程中的常见问题。需要学习如何应用并发编程的设计模式。 为了学习并发编程,可以参考一些优秀的Go语言并发编程书籍,比如《Go并发编程实战》、《Go语言高并发与微服务实战》等。同时也可以参考一些优秀的开源项目,如etcd、Docker等,深入了解Go语言并发编程的应用场景和实现方式。 ### 回答2: 学习Go语言并发编程可以按照以下步骤进行。 1. 学习并理解Go语言的并发模型:Go语言并发编程基于goroutine和channel。首先,需要了解goroutine的概念,它是Go语言中的一种轻量级线程。然后学习如何使用channel进行通信和同步。 2. 掌握goroutine的创建和管理:学习如何创建和管理goroutine,可以通过使用go关键字来创建一个新的goroutine,以便并发地执行任务。 3. 理解channel的使用:掌握channel的使用,了解如何创建、发送和接收数据。学习不同类型的channel以及它们在并发编程中的应用场景。 4. 学习互斥锁和读写锁:Go语言提供了互斥锁和读写锁来实现资源的安全访问。深入理解锁的概念和使用方法,学习如何避免并发访问导致的数据竞争。 5. 掌握并发编程的常见模式:学习并发编程中的常见模式,例如生产者-消费者模式、多路复用模式、线程池等。熟悉这些模式可以帮助我们更好地设计并发程序。 6. 阅读优秀的并发编程代码和文档:阅读优秀的并发编程代码可以提供实际的应用示例和启发。同时,阅读官方文档和相关书籍也是学习的重要途径。 7. 实践和调试:编写自己的并发代码,利用调试工具对程序进行调试,观察并发执行的过程和结果。通过实践来提高对并发编程的理解和应用能力。 总之,学习Go语言并发编程需要理解并发模型、掌握goroutine和channel的使用、了解锁的概念和使用方法,并通过实践来提高自己的技能。 ### 回答3: 学习Go语言并发编程可以通过以下几个步骤来进行。 首先,了解并发编程的概念和原则。并发编程是指同时进行多个任务,使用多个线程或协程来提高程序的效率和性能。了解并发编程的基本概念,如协程、锁、原子操作等,对学习Go语言并发编程非常重要。 其次,学习Go语言的并发特性。Go语言内置了丰富的并发编程工具和特性,如goroutine、channel、select等。通过学习这些特性,可以更好地理解Go语言中的并发编程模型,并能够正确地使用它们。 然后,阅读相关的书籍和文章。有很多经典的书籍和文章涉及到Go语言并发编程,这些资源可以帮助你更深入地理解并发编程的原理和实践。值得推荐的书籍有《Go语言实战》和《Go并发编程实战》。 接着,进行实践和项目练习。通过编写一些小型的并发程序和项目,可以将理论知识应用到实践中,加深对并发编程的理解和掌握。可以尝试使用goroutine和channel来实现一些并发任务,如爬虫、并发请求等。 最后,参与社区和交流。加入Go语言的社区,如论坛、聊天群等,与其他开发者交流和分享经验。通过与他人的交流,可以学习到更多实践中的经验和技巧,不断提升自己的并发编程能力。 总之,学习Go语言并发编程需要系统地学习基本概念和原则,掌握语言特性,进行实践和项目练习,并积极参与社区和交流。只有不断实践和学习,才能在并发编程领域不断进步。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值