深入浅出Go并发之协程—goroutine

简介: 深入浅出Go并发之协程—goroutine

1 梳理概念:进程、线程、协程

1.1 进程

在《计算机操作系统》一书中,进程这样被解释:

进程是进程实体的运行过程,是程序的基本执行实体,是系统进行资源分配和调度的一个独立单位。进程实体 = 程序段 + 相关数据段 + 进程控制块(PCB),

进程的特性:①动态性 ②并发性 ③独立性 ④异步性 ⑤结构性

进程的三种基本状态:就绪(Ready)状态、执行(Running)状态、阻塞(Block)状态

网络异常,图片无法展示
|


1.2 线程

线程是操作系统能够进行运算调度的最小单位。它被包含在进程之中,是进程中的实际运作单位。

同一进程中的多条线程将共享该进程中的全部系统资源,如虚拟地址空间,文件描述符和信号处理等等。但同一进程中的多个线程有各自的调用栈(call stack),自己的寄存器环境(register context),自己的线程本地存储(thread-local storage)。

一个进程可以有很多线程,每条线程并行执行不同的任务。

线程的基本状态:派生,阻塞,激活,调度,结束

网络异常,图片无法展示
|


1.3 协程

协程,又称微线程,纤程。英文名Coroutine。一句话说明什么是协程:协程是一种用户态的轻量级线程

协程拥有自己的寄存器上下文和栈。协程调度切换时,将寄存器上下文和栈保存到其他地方,在切回来的时候,恢复先前保存的寄存器上下文和栈。因此:协程能保留上一次调用时的状态(即所有局部状态的一个特定组合),每次过程重入时,就相当于进入上一次调用的状态,换种说法:进入上一次离开时所处逻辑流的位置。

在并发编程中,协程与线程类似,每个协程表示一个执行单元,有自己的本地数据,与其它协程共享全局数据和其它资源。

协程的基本状态:见下文。

2 为什么协程能更好支持并发

因为协程是用户自己来编写调度逻辑的,对CPU来说,协程其实是单线程,所以CPU不用去考虑怎么调度、切换上下文,这就省去了CPU的切换开销,所以协程在一定程度上又好于多线程。

对比Java的线程

内核线程 用户线程
Java线程 1 1
Go协程 m n

因此协程的好处

  • 无需线程上下文切换的开销
  • 无需原子操作锁定及同步的开销
  • 方便切换控制流,简化编程模型
  • 高并发+高扩展性+低成本:一个CPU支持上万的协程都不是问题。所以很适合用于高并发处理。

3 Go项目中goruntine的使用方式

3.1 简单使用

package main
func main() {
   go func() {
      println("Hello goruntine")
   }()
}
复制代码

网络异常,图片无法展示
|


为什么没有输出协程的输出语句,是因为主线程先执行完了,下面我们来试下:

package main
import "time"
func main() {
   go func() {
      println("Hello goruntine")
   }()
   time.Sleep(3 * time.Second)
}
复制代码

网络异常,图片无法展示
|


3.2 控制使用

控制协程的数量

(1)使用sync.WaitGroup控制协程数量

package main
import (
   "fmt"
   "sync"
   "time"
)
func main() {
   var wg sync.WaitGroup
   fmt.Println("This is main thread start ...")
   wg.Add(2)
   go func() {
      defer wg.Done()
      fmt.Println("Go 1 finished ...")
   }()
   go func() {
      defer wg.Done()
      time.Sleep(3 * time.Second)
      fmt.Println("Go 2 finished ...")
   }()
   wg.Wait()
   fmt.Println("This is main thread finished ...")
}
复制代码

网络异常,图片无法展示
|


(2)使用sync.Mutex控制协程数量

package main
import (
   "fmt"
   "sync"
)
func main() {
   var locker = new(sync.Mutex)
   var cond = sync.NewCond(locker)
   var done bool = false
   fmt.Println("Main is start...")
   cond.L.Lock()
   go func() {
      fmt.Println("Go is finished... ")
      done = true
      cond.Signal()
   }()
   fmt.Println("Main is wait...")
   if !done {
      cond.Wait()
      cond.L.Unlock()
   }
   fmt.Println("Main is finished...")
}
复制代码

4 goruntine的基本原理

4.1 源码

package runtime中的runtime2.go文件中:

//Stack描述了一个Go执行堆栈。堆栈的边界是[lo, hi),两边都没有隐式数据结构。  
type stack struct {
   lo uintptr //下界
   hi uintptr //上界
}
type g struct {
   // Stack信息.
   stack       stack   // offset known to runtime/cgo
   stackguard0 uintptr // offset known to liblink
   stackguard1 uintptr // offset known to liblink
   _panic       *_panic // innermost panic - offset known to liblink
   _defer       *_defer // innermost defer
   m            *m      // current m; offset known to arm liblink
   sched        gobuf   // goroutine切换时,用于保存g的上下文
   ......
}
复制代码

4.2 原理简述

通过go关键字调用底层函数runtime.newproc()创建一个goroutine

当调用该函数之后,goroutine会被设置成runnable状态

goroutine 本身只是一个数据结构,真正让 goroutine 运行起来的是调度器。Go 实现了一个用户态的调度器(GMP模型),这个调度器充分利用现代计算机的多核特性,同时让多个 goroutine 运行,同时 goroutine 设计的很轻量级,调度和上下文切换的代价都比较小。

4.3 goruntine状态

const (
   // G status
   _Gidle = iota //空闲中。G刚刚新建, 仍未初始化
   _Grunnable //待运行。就绪状态,G在运行队列中, 等待M取出并运行
   _Grunning //运行中。M正在运行这个G, 这时候M会拥有一个P
   _Gsyscall //系统调用中。M正在运行这个G发起的系统调用, 这时候M并不拥有P
   _Gwaiting // 等待中。G在等待某些条件完成, 这时候G不在运行也不在运行队列中(可能在channel的等待队列中)
   _Gmoribund_unused // 当前未使用。但在gdb中硬编码脚本。  
   _Gdead // 已中止。G未被使用, 可能已执行完毕
   _Genqueue_unused //  当前未使用。
   _Gcopystack // 栈复制中。G正在获取一个新的栈空间并把原来的内容复制过去(用于防止GC扫描)
   _Gpreempted // 停止自己来暂停抢占。 它类似于_Gwaiting,并且不在运行队列上,等待唤醒。
   // 以下状态为GC相关
   _Gscan          = 0x1000
   _Gscanrunnable  = _Gscan + _Grunnable  // 0x1001
   _Gscanrunning   = _Gscan + _Grunning   // 0x1002
   _Gscansyscall   = _Gscan + _Gsyscall   // 0x1003
   _Gscanwaiting   = _Gscan + _Gwaiting   // 0x1004
   _Gscanpreempted = _Gscan + _Gpreempted // 0x1009
)
复制代码

网络异常,图片无法展示
|


(图片链接:inews.gtimg.com/newsapp_bt/…

5 Go的重要概念:GMP模型

  • M代表一个内核线程,也可以称为一个工作线程。goroutine就是跑在M之上的。
  • P代表着处理器(processor) ,它的主要用途就是用来执行goroutine的,一个P代表执行一个go代码片段的基础(上下文环境),所以它也维护了一个可运行的goroutine队列,和自由的goroutine队列,里面存储了所有需要它来执行的goroutine。
  • G 代表着goroutine 实际的数据结构(就是你封装的那个方法) ,并维护者goroutine 需要的栈、程序计数器以及它所在的M等信息。
  • Seched代表着一个调度器,它维护有存储空闲的M队列和空闲的P队列,可运行的G队列,自由的G队列以及调度器的一些状态信息等。

5.1 用户协程(goroutine )

代码实现的协程,包含函数执行的指令和参数,任务对象,线程上下文切换,字段保护,和字段的寄存器。

type g struct {
   stack       stack   // offset known to runtime/cgo
   stackguard0 uintptr // offset known to liblink
   stackguard1 uintptr // offset known to liblink
   _panic       *_panic // innermost panic - offset known to liblink
   _defer       *_defer // innermost defer
   m            *m      // current m; offset known to arm liblink
   sched        gobuf
   ......
}
复制代码

5.2 内核线程(machine)

M是一个线程,每个M都有一个线程的栈。如果没有给线程的栈分配内存,操作系统会给线程的栈分配默认的内存。当线程的栈制定,M.stack->G.stack, M的PC寄存器会执行G提供的函数。

type m struct {
   g0      *g     // goroutine with scheduling stack
   morebuf gobuf  // gobuf arg to morestack
   divmod  uint32 // div/mod denominator for arm - known to liblink
   procid        uint64       // for debuggers, but offset not hard-coded
   gsignal       *g           // signal-handling g
   goSigStack    gsignalStack // Go-allocated signal handling stack
   sigmask       sigset       // storage for saved signal mask
   tls           [6]uintptr   // thread-local storage (for x86 extern register)
   mstartfn      func()
   curg          *g       // current running goroutine
   caughtsig     guintptr // goroutine running during fatal signal
   p             puintptr // attached p for executing go code (nil if not executing go code)
   nextp         puintptr
   oldp          puintptr // the p that was attached before executing a syscall
   id            int64
   mallocing     int32
   throwing      int32
   preemptoff    string // if != "", keep curg running on this m
   ......
}
复制代码

5.3 处理器(processor)

prosessor处理器是用来处理goroutine的,processor包含着每一个goroutine的资源。

一个Processor有很多goruntine组成的等待队列,但是一个Processor同一时刻只能执行一个G。

type p struct {
   id          int32
   status      uint32 // one of pidle/prunning/...
   link        puintptr
   schedtick   uint32     // incremented on every scheduler call
   syscalltick uint32     // incremented on every system call
   sysmontick  sysmontick // last tick observed by sysmon
   m           muintptr   // back-link to associated m (nil if idle)
   mcache      *mcache
   pcache      pageCache
   raceprocctx uintptr
   deferpool    [5][]*_defer // pool of available defer structs of different sizes (see panic.go)
   deferpoolbuf [5][32]*_defer
   // Cache of goroutine ids, amortizes accesses to runtime·sched.goidgen.
   goidcache    uint64
   goidcacheend uint64
   // Queue of runnable goroutines. Accessed without lock.
   runqhead uint32
   runqtail uint32
   runq     [256]guintptr
   ......
}
复制代码

5.4 调度器(schedt)

schedt主要工作就是创建 g。但是创建 g 的过程非常复杂,在调度 g 之前进行了多次判断:

  • 首先判断是否因为 gc 等待,如果是因为 gc 就等待 gc 结束。
  • 判断是否执行安全点函数。
type schedt struct {
   // accessed atomically. keep at top to ensure alignment on 32-bit systems.
   goidgen   uint64
   lastpoll  uint64 // time of last network poll, 0 if currently polling
   pollUntil uint64 // time to which current poll is sleeping
   lock mutex
   midle        muintptr // idle m's waiting for work
   nmidle       int32    // number of idle m's waiting for work
   nmidlelocked int32    // number of locked m's waiting for work
   mnext        int64    // number of m's that have been created and next M ID
   maxmcount    int32    // maximum number of m's allowed (or die)
   nmsys        int32    // number of system m's not counted for deadlock
   nmfreed      int64    // cumulative number of freed m's
   ngsys uint32 // number of system goroutines; updated atomically
   ...
}
复制代码

5.5 相互关系

首先创建一个G对象,然后G被保存在P的本地队列或者全局队列(global queue)。这时P会唤醒一个M。P按照它的执行顺序继续执行任务。M寻找一个空闲的P,如果找得到,将G移动到它自己。然后M执行一个调度循环:调用G对象->执行->清理线程->继续寻找Goroutine。

在M的执行过程中,上下文切换随时发生。当切换发生,任务的执行现场需要被保护,这样在下一次调度执行可以进行现场恢复。M的栈保存在G对象,只有现场恢复需要的寄存器(SP,PC等),需要被保存到G对象。

如果G对象还没有被执行,M可以将G重新放到P的调度队列,等待下一次的调度执行。当调度执行时,M可以通过G的vdsoSP, vdsoPC 寄存器进行现场恢复。

网络异常,图片无法展示
|


参考:

www.jianshu.com/p/eae8b66e7…

blog.csdn.net/weixin_4252…

www.cnblogs.com/freely/p/65…

www.golangroadmap.com/question_ba…

blog.csdn.net/guoweimelon…

blog.csdn.net/wsk8520/art…

zhuanlan.zhihu.com/p/261590663

www.cnblogs.com/ms27946/p/g…


相关文章
|
1天前
|
负载均衡 Go 调度
使用Go语言构建高性能的Web服务器:协程与Channel的深度解析
在追求高性能Web服务的今天,Go语言以其强大的并发性能和简洁的语法赢得了开发者的青睐。本文将深入探讨Go语言在构建高性能Web服务器方面的应用,特别是协程(goroutine)和通道(channel)这两个核心概念。我们将通过示例代码,展示如何利用协程处理并发请求,并通过通道实现协程间的通信和同步,从而构建出高效、稳定的Web服务器。
|
1天前
|
Cloud Native Go 云计算
多范式编程语言Go:并发与静态类型的结合
Go语言是Google于2007年开发的开源编程语言,旨在提高程序开发和部署的效率。它的独特特征在于结合了并发处理与静态类型系统,提供了简洁、高效、并行处理能力的编程体验。本文将探讨Go语言的特点、应用场景以及其在现代软件开发中的优势。
|
1天前
|
监控 负载均衡 算法
Golang深入浅出之-Go语言中的协程池设计与实现
【5月更文挑战第3天】本文探讨了Go语言中的协程池设计,用于管理goroutine并优化并发性能。协程池通过限制同时运行的goroutine数量防止资源耗尽,包括任务队列和工作协程两部分。基本实现思路涉及使用channel作为任务队列,固定数量的工作协程处理任务。文章还列举了一个简单的协程池实现示例,并讨论了常见问题如任务队列溢出、协程泄露和任务调度不均,提出了解决方案。通过合理设置缓冲区大小、确保资源释放、优化任务调度以及监控与调试,可以避免这些问题,提升系统性能和稳定性。
28 6
|
1天前
|
安全 Go
Golang深入浅出之-Go语言中的并发安全队列:实现与应用
【5月更文挑战第3天】本文探讨了Go语言中的并发安全队列,它是构建高性能并发系统的基础。文章介绍了两种实现方法:1) 使用`sync.Mutex`保护的简单队列,通过加锁解锁确保数据一致性;2) 使用通道(Channel)实现无锁队列,天生并发安全。同时,文中列举了并发编程中常见的死锁、数据竞争和通道阻塞问题,并给出了避免这些问题的策略,如明确锁边界、使用带缓冲通道、优雅处理关闭以及利用Go标准库。
26 5
|
1天前
|
存储 缓存 安全
Golang深入浅出之-Go语言中的并发安全容器:sync.Map与sync.Pool
Go语言中的`sync.Map`和`sync.Pool`是并发安全的容器。`sync.Map`提供并发安全的键值对存储,适合快速读取和少写入的情况。注意不要直接遍历Map,应使用`Range`方法。`sync.Pool`是对象池,用于缓存可重用对象,减少内存分配。使用时需注意对象生命周期管理和容量控制。在多goroutine环境下,这两个容器能提高性能和稳定性,但需根据场景谨慎使用,避免不当操作导致的问题。
35 4
|
1天前
|
安全 Go 开发者
Golang深入浅出之-Go语言中的CSP模型:深入理解并发哲学
【5月更文挑战第2天】Go语言的并发编程基于CSP模型,强调通过通信共享内存。核心概念是goroutines(轻量级线程)和channels(用于goroutines间安全数据传输)。常见问题包括数据竞争、死锁和goroutine管理。避免策略包括使用同步原语、复用channel和控制并发。示例展示了如何使用channel和`sync.WaitGroup`避免死锁。理解并发原则和正确应用CSP模型是编写高效安全并发程序的关键。
35 4
|
1天前
|
安全 Go 开发者
Golang深入浅出之-Go语言中的CSP模型:深入理解并发哲学
【5月更文挑战第1天】Go语言基于CSP理论,借助goroutines和channels实现独特的并发模型。Goroutine是轻量级线程,通过`go`关键字启动,而channels提供安全的通信机制。文章讨论了数据竞争、死锁和goroutine泄漏等问题及其避免方法,并提供了一个生产者消费者模型的代码示例。理解CSP和妥善处理并发问题对于编写高效、可靠的Go程序至关重要。
26 2
|
1天前
|
消息中间件 Go API
基于Go语言的微服务架构实践
随着云计算和容器化技术的兴起,微服务架构成为了现代软件开发的主流趋势。Go语言,以其高效的性能、简洁的语法和强大的并发处理能力,成为了构建微服务应用的理想选择。本文将探讨基于Go语言的微服务架构实践,包括微服务的设计原则、服务间的通信机制、以及Go语言在微服务架构中的优势和应用案例。
|
1天前
|
安全 测试技术 数据库连接
使用Go语言进行并发编程
【5月更文挑战第15天】Go语言以其简洁语法和强大的并发原语(goroutines、channels)成为并发编程的理想选择。Goroutines是轻量级线程,由Go运行时管理。Channels作为goroutine间的通信机制,确保安全的数据交换。在编写并发程序时,应遵循如通过通信共享内存、使用`sync`包同步、避免全局变量等最佳实践。理解并发与并行的区别,有效管理goroutine生命周期,并编写测试用例以确保代码的正确性,都是成功进行Go语言并发编程的关键。