Go 学习笔记(22)— 并发(01)[进程、线程、协程、并发和并行、goroutine 启动、goroutine 特点,runtime 包函数]

  • 进程、线程与协程。
    进程是操作系统资源分配的基本单位,线程是操作系统资源调度的基本单位。而协程位于用户态,是在线程基础上构建的轻量级调度单位。
  • 并发与并行。
    并行指的是同时做很多事情,并发是指同时管理很多事情。
  • 主协程与子协程。
    main 函数是特殊的主协程,它退出之后整个程序都会退出。而其他的协程都是子协程,子协程退出之后,程序正常运行。

Go 语言通过编译器运行时( runtime ),从语言上支持了并发的特性。

虽然 Go 程序编译后生成的是本地可执行代码,但是这些可执行代码必须运行在Go 语言的运行时(Runtime )中。Go 运行时类似 Java.NET 语言所用到的虚拟机,主要负责管理包括内存分配、自动垃圾回收、栈处理、协程(Goroutine)、信道(Channel,也称为通道)、切片(slice)、字典(map)和反射(reflect)等。Go 语言的 Runtime 运行机制可以用下图表示

Go 运行时
Go 运行时和用户编译后的代码被 Go 链接器(Linker )静态链接起来,形成一个可执行文件。从运行的角度来说,这个 Go 可执行文件由两部分组成:一部分是用户的代码,另一部分就是 Go 运行时。

Go 运行时通过接口函数调用来管理协程(Goroutine)和信道(Channel)等功能。Go 用户代码对操作系统内核 API 的调用会被 Go 运行时拦截并处理。

Go 运行时的重要组成部分是协程调度器(Goroutine Scheduler)。它负责追踪、调度每个协程运行,实际上是从应用程序的进程(Process)所属的线程池(Thread Pool)中分配一个线程执行这个协程。因此,与 Java 虚拟机中的 Java 线程和操作系统(OS)线程映射概念类似,每个协程只有分配到一个操作系统线程才能运行。

Go 语言的并发通过 goroutine 特性完成。 goroutine 类似于线程,但是可以根据需要创建多个 goroutine 并发工作。

goroutine 是由 Go 语言的运行时调度完成,而线程是由操作系统调度完成。

Go 语言的并发同步模型来自一个叫作通信顺序进程(Communicating Sequential Processes,CSP)的范型(paradigm)。CSP 是一种消息传递模型,通过在 goroutine 之间传递数据来传递消息,而不是对数据进行加锁来实现同步访问。

Go 放弃了传统的基于操作系统线程的并发模型,而采用了用户层轻量级线程,Go 将之称为 goroutine

goroutine 占用的资源非常小,Go 运行时默认为每个 goroutine 分配的栈空间仅 2KB。goroutine 调度的切换也不用陷入操作系统内核层完成,代价很低。因此,一个 Go 程序中可以创建成千上万个并发的 goroutine。而且,所有的 Go 代码都在 goroutine 中执行,哪怕是 go 运行时的代码也不例外。

在提供了开销较低的 goroutine 的同时,Go 还在语言层面内置了辅助并发设计的原语:channelselect。开发者可以通过语言内置的 channel 传递消息或实现同步,并通过 select 实现多路 channel 的并发控制。相较于传统复杂的线程并发模型,Go 对并发的原生支持将大大降低开发人员在开发并发程序时的心智负担。

goroutines 各自执行特定的工作,通过 channel+selectgoroutines 组合连接起来。并发的存在鼓励程序员在程序设计时进行独立计算的分解,而对并发的原生支持让 Go 语言也更适应现代计算环境。

1. 进程与线程

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

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

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

一个进程至少会包含一个线程。如果一个进程只包含了一个线程,那么它里面的所有代码都只会被串行地执行。每个进程的第一个线程都会随着该进程的启动而被创建,它们可以被称为其所属进程的主线程。

相对应的,如果一个进程中包含了多个线程,那么其中的代码就可以被并发地执行。除了进程的第一个线程之外,其他的线程都是由进程中已存在的线程创建出来的。

也就是说,主线程之外的其他线程都只能由代码显式地创建和销毁。

2. 协程与线程

协程:独立的栈空间,共享堆空间,调度由用户自己控制,本质上有点类似于用户级线程,这些用户级线程的调度也是自己实现的。

线程:一个线程上可以跑多个协程,协程是轻量级的线程。

线程与协程区别
为了突出这个 Go 协程模型的强大,我们可以来分析一个案例。假设有一台 web 服务器,每分钟处理 1000 个请求。如果必须同时运行每个请求,则意味着你需要创建 1000 个线程或将它们划分到不同的进程中。这就是经典服务器 Apache (https://www.apache.org/) 的做法,如果每个线程消耗 1MB 的堆栈大小,则意味着你将要使用 1GB 的内存用于处理改流量。当然,Apache 提供了 ThreadStackSize 指令来管理每个线程的堆栈大小,但是问题仍然没有得到根本的解决。

对于 Go 协程来说,由于堆栈大小可以动态增长,因此,你可以毫无问题的生成 1000 个 goruntine 。由于 goruntine 的初始堆栈空间可以调节,初始为 8KB(更高的 Go 版本可能会更小),因此并不会消耗多大的内存空间。同时当某个 goruntine 里面需要进行递归操作。Go 可以轻松的将堆栈大小调大,可以达到 1GB 的大小,这样无疑是“用更低的成本去做同样的事情”。

3. 并发和并行

Go 语言里的并发指的是能让某个函数独立于其他函数运行的能力。当一个函数创建为 goroutine 时, Go 会将其视为一个独立的工作单元。这个单元会被调度到可用的逻辑处理器上执行。

Go 语言运行时的调度器是一个复杂的软件,能管理被创建的所有 goroutine 并为其分配执行时间。这个调度器在操作系统之上,将操作系统的线程与语言运行时的逻辑处理器绑定,并在逻辑处理器上运行 goroutine

调度器在任何给定的时间,都会全面控制哪个 goroutine 要在哪个逻辑处理器上运行。

即便只有一个逻辑处理器, Go 也可以以神奇的效率和性能,并发调度无数个 goroutine

用于在 goroutine 之间同步和传递数据的关键数据类型叫作通道( channel )。

Go 语言不但有着独特的并发编程模型,以及用户级线程 goroutine,还拥有强大的用于调度 goroutine、对接系统级线程的调度器。

这个调度器是 Go 语言运行时系统的重要组成部分,它主要负责统筹调配 Go 并发编程模型中的三个主要元素,简称 GMP 模型(MPG 模型方便记忆),在这个模型中,

  • G 代表的是 Go 语言中的协程(Goroutine
  • M 代表的是实际的线程
  • P 代表的是 Go 逻辑处理器(Process)。

Go 语言为了方便协程调度与缓存,抽象出了逻辑处理器的概念。在任一时刻,一个 P 可能在本地包含多个 G,同时,一个 P 在任一时刻只能绑定一个 M

MPG模型
随着我们对协程、运行时协程调度的理解越来越深入,我们的知识组块、知识体系都可能会有所更新。例如,下面这个改进后的 GMP 模型就加入了本地运行队列和全局运行队列。它可以让我们更深入地看到调度器的运作模式,每个逻辑处理器 P 中都有单独的本地运行队列用于存储协程,这是为了减少并行时锁的使用。

同时我们也有全局共享的全局运行队列、本地运行队列可以获取全局运行队列中的协程,全局运行队列也可以接收本地运行队列中的协程。

MPG改进模型
来源:https://time.geekbang.org/column/article/579763

从宏观上说,GM 由于 P 的存在可以呈现出多对多的关系。当一个正在与某个 M 对接并运行着的 G,需要因某个事件(比如等待 I/O 或锁的解除)而暂停运行的时候,调度器总会及时地发现,并把这个 G 与那个 M 分离开,以释放计算资源供那些等待运行的 G 使用。

而当一个 G 需要恢复运行的时候,调度器又会尽快地为它寻找空闲的计算资源(包括 M)并安排运行。另外,当 M 不够用时,调度器会帮我们向操作系统申请新的系统级线程,而当某个 M 已无用时,调度器又会负责把它及时地销毁掉。

M、P、G 之间的关系(简化版)
参考:https://time.geekbang.org/column/article/39841

如果创建一个 goroutine 并准备运行,这个 goroutine 就会被放到调度器的全局运行队列中。之后,调度器就将这些队列中的 goroutine 分配给一个逻辑处理器,并放到这个逻辑处理器对应的本地运行队列中。本地运行队列中的 goroutine 会一直等待直到自己被分配的逻辑处理器执行。

Go调度器如何管理goroutine.jpg

摘自 《Go 语言实战》

有时,正在运行的 goroutine 需要执行一个阻塞的系统调用,如打开一个文件。当这类调用发生时,线程和 goroutine 会从逻辑处理器上分离,该线程会继续阻塞,等待系统调用的返回。与此同时,这个逻辑处理器就失去了用来运行的线程。

所以,调度器会创建一个新线程,并将其绑定到该逻辑处理器上。之后,调度器会从本地运行队列里选择另一个 goroutine 来运行。一旦被阻塞的系统调用执行完成并返回,对应的 goroutine 会放回到本地运行队列,而之前的线程会保存好,以便之后可以继续使用。

并发(concurrency)和并行(parallelism)区别:

  • 并行是让不同的代码片段同时在不同的物理处理器上执行。并行意味着程序在任意时刻都是同时运行的;多线程程序在多核心的 cpu 上运行,称为并行。
  • 并发是指同时管理很多事情,这些事情可能只做了一半就被暂停去做别的事情了。并发意味着程序在单位时间内是同时运行的多线程程序在单核心的 cpu 上运行,称为并发

并发与并行并不相同,并发主要由切换时间片来实现 “同时” 运行,并行则是直接利用多核实现多线程的运行,Go 程序可以设置使用核心数,以发挥多核计算机的能力。

在很多情况下,并发的效果比并行好,因为操作系统和硬件的总资源一般很少,但能支持系统同时做很多事情。

如果希望让 goroutine 并行,必须使用多于一个逻辑处理器。当有多个逻辑处理器时,调度器会将 goroutine 平等分配到每个逻辑处理器上。这会让 goroutine 在不同的线程上运行。

不过要想真的实现并行的效果,用户需要让自己的程序运行在有多个物理处理器的机器上。否则,哪怕 Go 语言运行时使用多个线程, goroutine 依然会在同一个物理处理器上并发运行,达不到并行的效果。

调度器包含一些聪明的算法,这些算法会随着 Go 语言的发布被更新和改进,所以不推荐盲目修改语言运行时对逻辑处理器的默认设置。

并发和并行.png

摘自 《Go 语言实战》

4. goroutine 启动

Go 语言的并发执行体称为 goroutine , Go 语言通过 go 关键字来启动一个 goroutinegoroutine 是一种非常轻量级的实现,可在单个进程里执行成千上万的并发任务。

goroutineGo 语言中的轻量级线程实现,由 Go 运行时( runtime )管理。 Go 程序会智能地将 goroutine 中的任务合理地分配给每个 CPU。

Go 程序从 main 包的 main() 函数开始,在程序启动时, Go 程序就会为 main() 函数创建一个默认的 goroutine

Go 程序中使用 go 关键字为一个函数创建一个 goroutine 。一个函数可以被创建多个 goroutine ,而一个 goroutine 必定对应一个函数。

创建一个 goroutine 的格式如下:

go functionName( parameterList )

注意: go 关键字后面必须跟一个函数,不能是语句或其他东西,函数的返回值被忽略。如下代码为启动一个匿名函数的 goroutine

go func() {
	println("Hello, World!")
}()

调度器不能保证多个 goroutine 执行次序,且进程退出时不会等待它们结束。

4.1 匿名函数启动 goroutine

使用匿名函数或闭包创建 goroutine 时,除了将函数定义部分写在 go 的后面之外,还需要加上匿名函数的调用参数,格式如下:

go func( 参数列表 ){
    函数体
}( 调用参数列表 )

匿名函数创建 goroutine 的例子:

package main

import (
	"runtime"
	"time"
)

func main() {
	go func() {
		sum := 0
		for i := 0; i <= 10000; i++ {
			sum += i
		}

		println("sum is: ", sum)
		time.Sleep(1 * time.Second)
	}()
	//NumGoroutine 可以返回当前程序的 goroutine 数目
	println("NumGoroutine=", runtime.NumGoroutine())

	// main goroutine 故意“ sleep ” 5 秒, 防止 main 提前退出
	time.Sleep(5 * time.Second)
}

输出结果:

NumGoroutine= 2
sum is:  50005000

4.2 有名函数启动 goroutine

package main

import (
	"runtime"
	"time"
)

func sum() {
	sum := 0
	for i := 0; i <= 10000; i++ {
		sum += i
	}

	println("sum is: ", sum)
	time.Sleep(1 * time.Second)
}

func main() {
	go sum()
	//NumGoroutine 可以返回当前程序的 goroutine 数目
	println("NumGoroutine=", runtime.NumGoroutine())

	// main goroutine 故意“ sleep ” 5 秒, 防止 main 提前退出
	time.Sleep(5 * time.Second)
}

输出结果:

NumGoroutine= 2
sum is:  50005000

5. goroutine 特点

  1. go 的执行是非阻塞的,不会等待
f()  	//	调用f()函数,并等待f()返回
go f()	//	创建一个新的goroutine去执行f(),不需要等待
  1. go 后面的函数的返回值会被忽略

  2. 调度器不能保证多个 goroutine 的执行次序

  3. 没有父子 goroutine 的概念,所有的 goroutine 是平等地被调度和执行的

  4. Go 程序在执行时会单独为 main 函数创建一个 goroutine ,遇到其他 go 关键字时再去创建其他的 goroutine

  5. 主函数返回时,所有的 goroutine 都会被直接打断,程序退出;

  6. Go 没有暴露 goroutine id 给用户,所以不能在一个 goroutine 里面显式地操作另一个 goroutine , 不过 runtime 包提供了一些函数访问和设置 goroutine 的相关信息;

  7. runtime.NumGoroutine 返回一个进程的所有 goroutine 数, main()goroutine 也被算在里面。因此实际创建的 goroutine 数量为扣除 main()goroutine 数。

package main

import "fmt"

func main() {
	for i := 0; i < 10; i++ {
		go func() {
			fmt.Println(i)
		}()
	}
}

该代码不会有任何内容打印出来。

因为:一旦主 goroutine 中的代码(也就是 main 函数中的那些代码)执行完毕,当前的 Go 程序就会结束运行。如此一来,如果在 Go 程序结束的那一刻,还有 goroutine 未得到运行机会,那么它们就真的没有运行机会了,它们中的代码也就不会被执行了。

严谨地讲,Go 语言并不会去保证这些 goroutine 会以怎样的顺序运行。由于主 goroutine 会与我们手动启用的其他 goroutine 一起接受调度,又因为调度器很可能会在 goroutine 中的代码只执行了一部分的时候暂停,以期所有的 goroutine 有更公平的运行机会。

所以哪个 goroutine 先执行完、哪个 goroutine 后执行完往往是不可预知的。

6. runtime 包函数

Go 语言程序运行时( runtime )实现了一个小型的任务调度器。这套调度器的工作原理类似于操作系统调度线程, Go 程序调度器可以高效地将 CPU 资源分配给每一个任务。

6.1 func GOMAXPROCS

传统逻辑中,开发者需要维护线程池中线程与 CPU 核心数量的对应关系。同样的, Go 地中也可以通过 runtime.GOMAXPROCS() 函数做到,格式为:

runtime.GOMAXPROCS(逻辑CPU数量)

这里的逻辑 CPU 数量可以有如下几种数值:

  • =0:查询当前的 GOMAXPROCS 值。
  • =1:设置单核心执行。
  • .> 1:设置多核并发执行。

func GOMAXPROCS(n int) int 用来设置或查询可以井发执行的 goroutine 数目, n 大于 1 表示设置 GOMAXPROCS 值, 否则表示查询当前的 GOMAXPROCS 值。例如:

package main

import (
	"runtime"
)

func main() {

	// 获取当前的 GOMAXPROCS 值
	println("GOMAXPROCS=", runtime.GOMAXPROCS(0))

	// 设置当前的 GOMAXPROCS 值
	runtime.GOMAXPROCS(2)

	// 获取当前的 GOMAXPROCS 值
	println("GOMAXPROCS=", runtime.GOMAXPROCS(0))
}

输出结果:

GOMAXPROCS= 1
GOMAXPROCS= 2

Go 1.5 版本之前,默认使用的是单核心执行。从 Go 1.5 版本开始,默认执行上面语句以便让代码并发执行,最大效率地利用 CPU

GOMAXPROCS 同时也是一个环境变量,在应用程序启动前设置环境变量也可以起到相同的作用。

6.2 func Goexit

func Goexit () 是结束当前 goroutine 的运行, Goexit 在结束当前 goroutine 运行之前会调用当前 goroutine 已经注册的 defer

Goexit 并不会产生 panic ,所以该 goroutine defer 里面的 recover 调用都返回 nil 。

调用 runtime.Goexit 将立即终止当前 goroutine 执行,调度器确保所有已注册 defer 延迟调用被执行。

package main

import (
	"runtime"
	"sync"
)

func main() {
	wg := new(sync.WaitGroup)
	wg.Add(1)
	go func() {
		defer wg.Done()
		defer println("A.defer")
		func() {
			defer println("B.defer")
			runtime.Goexit() // 终止当前 goroutine
			println("B")     // 不会执行
		}()
		println("A") // 不会执行
	}()
	wg.Wait()
}

输出:

B.defer
A.defer

6.3 func Gosched

goroutine 可能的切换点:

  1. I/Oselect
  2. channel
  3. 等待锁
  4. 函数调用(有时)
  5. runetime.Gosched()

和协程 yield 作用类似, Gosched 是放弃当前调度执行机会,将当前 goroutine 暂停,放回队列等待下次被调度执行。

package main

import (
	"runtime"
	"sync"
)

func main() {
	wg := new(sync.WaitGroup)
	wg.Add(2)

	go func() {
		defer wg.Done()
		for i := 0; i < 5; i++ {
			println("Hello, World!")
		}

	}()
	go func() {
		defer wg.Done()
		for i := 0; i < 5; i++ {
			println(i)
			if i == 2 {
				runtime.Gosched()
			}
		}
	}()

	println("NumGoroutine is ", runtime.GOMAXPROCS(0))
	wg.Wait()
}

输出:

NumGoroutine is  1
0
1
2
Hello, World!
Hello, World!
Hello, World!
Hello, World!
Hello, World!
3
4

runtime.Gosched() 用于让出 CPU 时间片。这就像跑接力赛,A 跑了一会碰到代码, runtime.Gosched() 就把接力棒交给 B 了,A 歇着了,B 继续跑。

package main

import (
	"fmt"
	"runtime"
)

func say(s string) {
	for i := 0; i < 2; i++ {
		runtime.Gosched()
		fmt.Println(s)
	}
}
func main() {
	go say("world")
	say("hello")
}

输出:

hello
world
hello

runtime.Gosched() 会在不同的 goroutine 之间切换,当 main goroutine 退出时,其它的 goroutine 都会直接退出,所以没有机会第二次打印 word 把代码中的 runtime.Gosched() 注释掉,执行结果是:

hello
hello

因为还没等待 say(“world”) 执行, main goroutine 已经执行完成并退出了。

这里同时可以看出, go 中的 goroutine 并不是同时在运行。事实上,如果没有在代码中通过

	runtime.GOMAXPROCS(n)

其中 n 是整数,指定使用多核的话,goroutine 都是在一个线程里的,它们之间通过不停的让出时间片轮流运行,达到类似同时运行的效果。

当一个 goroutine 发生阻塞, Go 会自动地把与该 goroutine 处于同一系统线程的其他 goroutines 转移到另一个系统线程上去,以使这些 goroutines 不阻塞。

6.4 一个 逻辑处理器处理 goroutine 时间较短

// 这个示例程序展示如何创建goroutine 以及调度器的行为
package main

import (
	"fmt"
	"runtime"
	"sync"
)

// main是所有Go程序的入口
func main() {
	// 分配一个逻辑处理器给调度器使用, 这个函数允许程序更改调度器可以使用的逻辑处理器的数量。
	runtime.GOMAXPROCS(1)

	// wg用来等待程序完成
	// WaitGroup是一个计数信号量,可以用来记录并维护运行的 goroutine。如果WaitGroup的值大于0,Wait方法就会阻塞。
	var wg sync.WaitGroup

	// 计数加2,表示要等待两个goroutine
	wg.Add(2)

	fmt.Println("Start Goroutines")

	// 声明一个匿名函数,并创建一个goroutine
	go func() {
		// 关键字defer会修改函数调用时机,在正在执行的函数返回时才真正调用defer声明的函数。
		// 关键字defer保证,每个 goroutine 一旦完成其工作就调用Done方法。
		// 在函数退出时调用Done来通知main函数工作已经完成
		defer wg.Done()

		// 显示字母表3次
		for count := 0; count < 3; count++ {
			for char := 'a'; char < 'a'+26; char++ {
				fmt.Printf("%c ", char)
			}
		}
	}()

	// 声明一个匿名函数,并创建一个goroutine
	go func() {
		// 在函数退出时调用Done来通知main函数工作已经完成
		defer wg.Done()

		// 显示字母表3次
		for count := 0; count < 3; count++ {
			for char := 'A'; char < 'A'+26; char++ {
				fmt.Printf("%c ", char)
			}
		}
	}()

	fmt.Println("Waiting To Finish")
	// 等待goroutine结束,否则 main 函数将直接继续往下走
	wg.Wait()

	fmt.Println("\nTerminating Program")
}

输出:

Start Goroutines
Waiting To Finish
A B C D E F G H I J K L M N O P Q R S T U V W X Y Z A B C D E F G H I J K L M N O P Q R S T U V W X Y Z A B C D E F G H I J K L M N O P Q R S T U V W X Y Z a b c d e f g h i j k l m n o p q r s t u v w x y z a b c d e f g h i j k l m n o p q r s t u v w x y z a b c d e f g h i j k l m n o p q r s t u v w x y z 
Terminating Program

第一个 goroutine 完成所有显示需要花时间太短了,以至于在调度器切换到第二个 goroutine 之前,就完成了所有任务。这也是为什么会看到先输出了所有的大写字母,之后才输出小写字母。我们创建的两个 goroutine 一个接一个地并发运行,独立完成显示字母表的任务。

6.5 一个 逻辑处理器处理 goroutine 时间较长

基于调度器的内部算法,一个正运行的 goroutine 在工作结束前,可以被停止并重新调度。

调度器这样做的目的是防止某个 goroutine 长时间占用逻辑处理器。当 goroutine 占用时间过长时,调度器会停止当前正运行的 goroutine ,并给其他可运行的 goroutine 运行的机会。

图6-4从逻辑处理器的角度展示了这一场景。在第1步,调度器开始运行 goroutine A,而 goroutine B 在运行队列里等待调度。之后,在第2步,调度器交换了 goroutine A 和 goroutine B。由于 goroutine A 并没有完成工作,因此被放回到运行队列。之后,在第3步,goroutine B 完成了它的工作并被系统销毁。这也让 goroutine A 继续之前的工作。

21acf4e0-e402-11e7-aa01-8f0875907fad.png

图: goroutine 在逻辑处理器的线程上进行交换

// 这个示例程序展示如何创建goroutine 以及调度器的行为
package main

import (
	"fmt"
	"runtime"
	"sync"
)

var wg sync.WaitGroup

// main是所有Go程序的入口
func main() {
	// 分配一个逻辑处理器给调度器使用, 这个函数允许程序更改调度器可以使用的逻辑处理器的数量。
	runtime.GOMAXPROCS(1)

	// wg用来等待程序完成
	// WaitGroup是一个计数信号量,可以用来记录并维护运行的 goroutine。如果WaitGroup的值大于0,Wait方法就会阻塞。

	// 计数加2,表示要等待两个goroutine
	wg.Add(2)

	fmt.Println("Start Goroutines")

	// 创建两个goroutine
	go printPrime("A")
	go printPrime("B")
	fmt.Println("Waiting To Finish")
	// 等待goroutine结束,否则 main 函数将直接继续往下走
	wg.Wait()

	fmt.Println("\nTerminating Program")
}

func printPrime(prefix string) {
	defer wg.Done()
next:
	for outer := 2; outer < 50000; outer++ {
		for inner := 2; inner < outer; inner++ {
			if outer%inner == 0 {
				continue next
			}
		}
		fmt.Printf("%s:%d\n", prefix, outer)
	}
	fmt.Println("Completed", prefix)
}

上述程序创建了两个 goroutine ,分别打印 1~50000 内的素数。查找并显示素数会消耗不少时间,这会让调度器有机会在第一个 goroutine 找到所有素数之前,切换到其它 goroutine

输出:

Start Goroutines
Waiting To Finish
B:2
B:3
B:5
...
B:8369
B:8377
B:8387
B:8389
A:2             ** 切换goroutine
A:3
A:5
...
A:3727
A:3733
A:3739
B:8419          ** 切换goroutine
B:8423
B:8429
...
B:10567
B:10589
B:10597
B:10601
A:3761          ** 切换goroutine
A:3767
A:3769
.....

Go 标准库的 runtime 包里有一个名为 GOMAXPROCS 的函数,通过它可以指定调度器可用的逻辑处理器的数量。用这个函数,可以给每个可用的物理处理器在运行的时候分配一个逻辑处理器。

import "runtime"

// 给每个可用的核心分配一个逻辑处理器
runtime.GOMAXPROCS(runtime.NumCPU())

runtime 提供了修改 Go 语言运行时配置参数的能力。

在上述代码中,我们使用两个 runtime 包的函数来修改调度器使用的逻辑处理器的数量。函数 NumCPU 返回可以使用的物理处理器的数量。

因此,调用 GOMAXPROCS 函数就为每个可用的物理处理器创建一个逻辑处理器。**需要强调的是,使用多个逻辑处理器并不意味着性能更好。**在修改任何语言运行时配置参数的时候,都需要配合基准测试来评估程序的运行效果。

只有在有多个逻辑处理器且可以同时让每个 goroutine 运行在一个可用的物理处理器上的时候, goroutine 才会并行运行

  • 3
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值