Go基础:goroutine使用、调度、runtime包

简介: Go基础:goroutine使用、调度、runtime包

目录

并发编程前言

进程和线程

并发和并行

协程和线程

Goroutine

使用goroutine

启动多个goroutine

goroutine与线程

可增长的栈

goroutine调度

runtime包

runtime.Gosched()

runtime.Goexit()

runtime.GOMAXPROCS



并发编程前言

进程和线程

   A. 进程是程序在操作系统中的一次执行过程,系统进行资源分配和调度的一个独立单位。

   B. 线程是进程的一个执行实体,是CPU调度和分派的基本单位,它是比进程更小的能独立运行的基本单位。

   C.一个进程可以创建和撤销多个线程;同一个进程中的多个线程之间可以并发执行。


并发和并行

   A. 多线程程序在一个核的cpu上运行,就是并发。

   B. 多线程程序在多个核的cpu上运行,就是并行。


协程和线程

协程

协程

协程

  • 协程:独立的栈空间,共享堆空间,调度由用户自己控制,本质上有点类似于用户级线程,这些用户级线程的调度也是自己实现的。
  • 线程:一个线程上可以跑多个协程,协程是轻量级的线程。
  • goroutine 只是由官方实现的超级"线程池"。
  • 每个实力4~5KB的栈内存占用和由于实现机制而大幅减少的创建和销毁开销是go高并发的根本原因
  • 并发不是并行:
  • 并发主要由切换时间片来实现"同时"运行,并行则是直接利用多核实现多线程的运行,go可以设置使用核数,以发挥多核计算机的能力。
  • goroutine 奉行通过通信来共享内存,而不是共享内存来通信


Goroutine

goroutine


Go语言中的goroutine就是这样一种机制,goroutine的概念类似于线程,但 goroutine是由Go的运行时(runtime)调度和管理的。Go程序会智能地将 goroutine 中的任务合理地分配给每个CPU。Go语言之所以被称为现代化的编程语言,就是因为它在语言层面已经内置了调度和上下文切换的机制。


在Go语言编程中你不需要去自己写进程、线程、协程,你的技能包里只有一个技能–goroutine,当你需要让某个任务并发执行的时候,你只需要把这个任务包装成一个函数,开启一个goroutine去执行这个函数就可以了,就是这么简单粗暴。

Go程序会为main()函数创建一个默认的goroutine

goroutine


使用goroutine

Go语言中使用goroutine非常简单,只需要在调用函数的时候在前面加上go关键字,就可以为一个函数创建一个goroutine。

一个goroutine必定对应一个函数,可以创建多个goroutine去执行相同的函数。


启动多个goroutine

goroutine切换点


在Go语言中实现并发就是这样简单,我们还可以启动多个goroutine。让我们再来一个例子: (这里使用了sync.WaitGroup来实现goroutine的同步)

var wg sync.WaitGroup
func hello(i int) {
    defer wg.Done() // goroutine结束就登记-1
    fmt.Println("Hello Goroutine!", i)
}
func main() {
    for i := 0; i < 10; i++ {
        wg.Add(1) // 启动一个goroutine就登记+1
        go hello(i)
    }
    wg.Wait() // 等待所有登记的goroutine都结束
}


多次执行上面的代码,会发现每次打印的数字的顺序都不一致。这是因为10个goroutine是并发执行的,而goroutine的调度是随机的。

注意

  • 如果主协程退出了,其他任务还执行吗(运行下面的代码测试一下吧)
package main
import (
  "fmt"
  "time"
)
func main() {
  // 合起来写
  go func() {
    i := 0
    for {
      i++
      fmt.Printf("new goroutine: i = %d\n", i)
      time.Sleep(time.Second)
    }
  }()
  i := 0
  for {
    i++
    fmt.Printf("main goroutine: i = %d\n", i)
    time.Sleep(time.Second)
    if i == 2 {
      break
    }
  }
}
// 返回结果
//main goroutine: i = 1
//new goroutine: i = 1
//new goroutine: i = 2
//main goroutine: i = 2


goroutine与线程

可增长的栈

OS线程(操作系统线程)一般都有固定的栈内存(通常为2MB),一个goroutine的栈在其生命周期开始时只有很小的栈(典型情况下2KB),goroutine的栈不是固定的,他可以按需增大和缩小,goroutine的栈大小限制可以达到1GB,虽然极少会用到这个大。所以在Go语言中一次创建十万左右的goroutine也是可以的。


goroutine调度

GPM是Go语言运行时(runtime)层面的实现,是go语言自己实现的一套调度系统。区别于操作系统调度OS线程。

  • 1.G很好理解,就是个goroutine的,里面除了存放本goroutine信息外 还有与所在P的绑定等信息
  • 2.P管理着一组goroutine队列,P里面会存储当前goroutine运行的上下文环境(函数指针,堆栈地址及地址边界),P会对自己管理的goroutine队列做一些调度(比如把占用CPU时间较长的goroutine暂停、运行后续的goroutine等等)当自己的队列消费完了就去全局队列里取,如果全局队列里也消费完了会去其他P的队列里抢任务。
  • 3.M(machine)是Go运行时(runtime)对操作系统内核线程的虚拟, M与内核线程一般是一一映射的关系, 一个groutine最终是要放到M上执行的;


P与M一般也是一一对应的。他们关系是: P管理着一组G挂载在M上运行。当一个G长久阻塞在一个M上时,runtime会新建一个M,阻塞G所在的P会把其他的G 挂载在新建的M上。当旧的G阻塞完成或者认为其已经死掉时 回收旧的M。


P的个数是通过runtime.GOMAXPROCS设定(最大256),Go1.5版本之后默认为物理线程数。 在并发量大的时候会增加一些P和M,但不会太多,切换太频繁的话得不偿失。

单从线程调度讲,Go语言相比起其他语言的优势在于OS线程是由OS内核来调度的,goroutine则是由Go运行时(runtime)自己的调度器调度的,这个调度器使用一个称为m:n调度的技术(复用/调度m个goroutine到n个OS线程)。 其一大特点是


goroutine的调度是在用户态下完成的, 不涉及内核态与用户态之间的频繁切换,包括内存的分配与释放,都是在用户态维护着一块大的内存池, 不直接调用系统的malloc函数(除非内存池需要改变),成本比调度OS线程低很多。 另一方面充分利用了多核的硬件资源,近似的把若干goroutine均分在物理线程上, 再加上本身goroutine的超轻量,以上种种保证了go调度方面的性能。

   

runtime包

runtime.Gosched()

让出CPU时间片,重新等待安排任务

package main
import (
  "fmt"
  "runtime"
)
func main() {
  go func(s string) {
    for i := 0; i < 2; i++ {
      fmt.Println(s)
    }
  }("world")
  // 主协程
  for i := 0; i < 2; i++ {
    // 切一下,再次分配任务
    runtime.Gosched()
    fmt.Println("hello")
  }
}
// 返回结果
//world
//world
//hello
//hello


runtime.Goexit()

退出当前协程

package main
import (
  "fmt"
  "runtime"
)
func main() {
  go func() {
    defer fmt.Println("A.defer")
    func() {
      defer fmt.Println("B.defer")
      // 结束协程
      runtime.Goexit()
      defer fmt.Println("C.defer")
      fmt.Println("B")
    }()
    fmt.Println("A")
  }()
  select {}
}
// 返回结果
//B.defer
//A.defer
//fatal error: all goroutines are asleep - deadlock!
//
//goroutine 1 [select (no cases)]:
//main.main()


runtime.GOMAXPROCS

  • Go运行时的调度器使用GOMAXPROCS参数来确定需要使用多少个OS线程来同时执行Go代码默认值是机器上的CPU核心数。例如在一个8核心的机器上,调度器会把Go代码同时调度到8个OS线程上(GOMAXPROCS是m:n调度中的n)。
  • Go语言中可以通过runtime.GOMAXPROCS()函数设置当前程序并发时占用的CPU逻辑核心数。
  • Go1.5版本之前,默认使用的是单核心执行。Go1.5版本之后,默认使用全部的CPU逻辑核心数。

我们可以通过将任务分配到不同的CPU逻辑核心上实现并行的效果,这里举个例子:

package main
import (
  "fmt"
  "runtime"
  "time"
)
func a() {
  for i := 1; i < 10; i++ {
    fmt.Println("A:", i)
  }
}
func b() {
  for i := 1; i < 10; i++ {
    fmt.Println("B:", i)
  }
}
func main() {
  runtime.GOMAXPROCS(1)
  go a()
  go b()
  time.Sleep(time.Second)
}
// 返回结果
//A: 1
//A: 2
//A: 3
//A: 4
//A: 5
//A: 6
//A: 7
//A: 8
//A: 9
//B: 1
//B: 2
//B: 3
//B: 4
//B: 5
//B: 6
//B: 7
//B: 8
//B: 9


两个任务只有一个逻辑核心,此时是做完一个任务再做另一个任务。 将逻辑核心数设为2,此时两个任务并行执行,代码如下。

package main
import (
  "fmt"
  "runtime"
  "time"
)
func a() {
  for i := 1; i < 10; i++ {
    fmt.Println("A:", i)
  }
}
func b() {
  for i := 1; i < 10; i++ {
    fmt.Println("B:", i)
  }
}
func main() {
  runtime.GOMAXPROCS(2)
  go a()
  go b()
  time.Sleep(time.Second)
}
// 返回结果
//B: 1
//B: 2
//B: 3
//B: 4
//B: 5
//B: 6
//B: 7
//B: 8
//B: 9
//A: 1
//A: 2
//A: 3
//A: 4
//A: 5
//A: 6
//A: 7
//A: 8
//A: 9
//
//Process finished with exit code 0


Go语言中的操作系统线程和goroutine的关系:

  • 1.一个操作系统线程对应用户态多个goroutine。
  • 2.go程序可以同时使用多个操作系统线程。
  • 3.goroutine和OS线程是多对多的关系,即m:n。



目录
相关文章
|
1月前
|
Go 调度 开发者
CSP模型与Goroutine调度的协同作用:构建高效并发的Go语言世界
【2月更文挑战第17天】在Go语言的并发编程中,CSP模型与Goroutine调度机制相互协同,共同构建了高效并发的运行环境。CSP模型通过通道(channel)实现了进程间的通信与同步,而Goroutine调度机制则确保了并发任务的合理调度与执行。本文将深入探讨CSP模型与Goroutine调度的协同作用,分析它们如何共同促进Go语言并发性能的提升。
|
2月前
|
并行计算 算法 安全
通过三个例子,学习 Go 语言并发编程的利器 - goroutine
通过三个例子,学习 Go 语言并发编程的利器 - goroutine
42 0
|
3月前
|
固态存储 测试技术 Go
Go语言 os包 不可不知的性能排行榜
Go语言 os包 不可不知的性能排行榜
59 0
|
3月前
|
Go
高效Go语言编程:os包实用技术大揭示
高效Go语言编程:os包实用技术大揭示
44 0
|
2天前
|
Go 开发者
Golang深入浅出之-Go语言上下文(context)包:处理取消与超时
【4月更文挑战第23天】Go语言的`context`包提供`Context`接口用于处理任务取消、超时和截止日期。通过传递`Context`对象,开发者能轻松实现复杂控制流。本文解析`context`包特性,讨论常见问题和解决方案,并给出代码示例。关键点包括:1) 确保将`Context`传递给所有相关任务;2) 根据需求选择适当的`Context`创建函数;3) 定期检查`Done()`通道以响应取消请求。正确使用`context`包能提升Go程序的控制流管理效率。
7 1
|
2天前
|
安全 Go 开发者
Golang深入浅出之-Go语言并发编程面试:Goroutine简介与创建
【4月更文挑战第22天】Go语言的Goroutine是其并发模型的核心,是一种轻量级线程,能低成本创建和销毁,支持并发和并行执行。创建Goroutine使用`go`关键字,如`go sayHello(&quot;Alice&quot;)`。常见问题包括忘记使用`go`关键字、不正确处理通道同步和关闭、以及Goroutine泄漏。解决方法包括确保使用`go`启动函数、在发送完数据后关闭通道、设置Goroutine退出条件。理解并掌握这些能帮助开发者编写高效、安全的并发程序。
13 1
|
5天前
|
编译器 Go 开发者
Go语言入门|包、关键字和标识符
Go语言入门|包、关键字和标识符
22 0
|
1月前
|
程序员 Go 数据处理
|
1月前
|
数据可视化 Go
|
2月前
|
Go C语言
安装go-sqlite3包时报exec: "gcc": executable file not found in %PATH%解决办法
安装go-sqlite3包时报exec: "gcc": executable file not found in %PATH%解决办法