《Go in action》读后记录:Go的并发与并行
Posted 张伯雨
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了《Go in action》读后记录:Go的并发与并行相关的知识,希望对你有一定的参考价值。
一、使用goroutine来运行程序
1.Go的并发与并行
Go的并发能力,是指让某个函数独立于其他函数运行的能力。当为一个函数创建goroutine
时,该函数将作为一个独立的工作单元,被 调度器 调度到可用的逻辑处理器上执行。Go的运行时调度器是个复杂的软件,它做的工作大致是:
- 管理被创建的所有goroutine,为其分配执行时间
- 将操作系统线程与语言运行时的逻辑处理器绑定
参考The Go scheduler ,这里较浅显地说一下Go的运行时调度器。操作系统会在物理处理器上调度操作系统线程
来运行,而Go语言的运行时会在逻辑处理器
上调度goroutine
来运行,每个逻辑处理器都分别绑定到单个操作系统线程上。这里涉及到三个角色:
- M:操作系统线程,这是真正的内核OS线程
- P:逻辑处理器,代表着调度的上下文,它使goroutine在一个M上跑
- G:goroutine,拥有自己的栈,指令指针等信息,被P调度
每个P会维护一个全局运行队列(称为runqueue),处于ready就绪状态的goroutine
(灰色G)被放在这个队列中等待被调度。在编写程序时,每当go func
启动一个goroutine
时,runqueue
便在尾部加入一个goroutine
。在下一个调度点上,P就从runqueue
中取出一个goroutine
出来执行(蓝色G)。
当某个操作系统线程M阻塞的时候(比如goroutine
执行了阻塞的系统调用),P可以绑定到另外一个操作系统线程M上,让运行队列中的其他goroutine
继续执行:
上图中G0执行了阻塞操作,M0被阻塞,P将在新的系统线程M1上继续调度G执行。M1有可能是被新创建的,或者是从线程缓存中取出。Go调度器保证有足够的线程来运行所有的P,语言运行时默认限制每个程序最多创建10000个线程,这个现在可以通过调用runtime/debug包的SetMaxThreads
方法来更改。
Go可以在在一个逻辑处理器P上实现并发,如果需要并行,必须使用多于1个的逻辑处理器。Go调度器会把goroutine
平等分配到每个逻辑处理器上,此时goroutine
将在不同的线程上运行,不过前提是要求机器拥有多个物理处理器。
2.创建goroutine
使用关键字go
来创建一个goroutine
,并让所有的goroutine
都得到执行:
//example1.go
package main
import (
"runtime"
"sync"
"fmt"
)
var (
wg sync.WaitGroup
)
func main() {
//分配一个逻辑处理器P给调度器使用
runtime.GOMAXPROCS(1)
//在这里,wg用于等待程序完成,计数器加2,表示要等待两个goroutine
wg.Add(2)
//声明1个匿名函数,并创建一个goroutine
fmt.Printf("Begin Coroutines\\n")
go func() {
//在函数退出时,wg计数器减1
defer wg.Done()
//打印3次小写字母表
for count := 0; count < 3; count++ {
for char := \'a\'; char < \'a\'+26; char++ {
fmt.Printf("%c ", char)
}
}
}()
//声明1个匿名函数,并创建一个goroutine
go func() {
defer wg.Done()
//打印大写字母表3次
for count := 0; count < 3; count++ {
for char := \'A\'; char < \'A\'+26; char++ {
fmt.Printf("%c ", char)
}
}
}()
fmt.Printf("Waiting To Finish\\n")
//等待2个goroutine执行完毕
wg.Wait()
}
这个程序使用runtime.GOMAXPROCS(1)
来分配一个逻辑处理器给调度器使用,两个goroutine
将被该逻辑处理器调度并发执行。程序输出:
Begin Coroutines
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
从输出来看,是先执行完一个goroutine
,再接着执行第二个goroutine
的,大写字母全部打印完后,再打印全部的小写字母。那么,有没有办法让两个goroutine
并行执行呢?为程序指定两个逻辑处理器即可:
//修改为2个逻辑处理器
runtime.GOMAXPROCS(2)
此时执行程序,输出为:
Begin Coroutines
Waiting To Finish
A B C D E 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 F G H I J K L M N O P Q R S T U V W X d e f g h i j k l m n o p q r s 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 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
那如果只有1个逻辑处理器,如何让两个goroutine交替被调度?实际上,如果goroutine
需要很长的时间才能运行完,调度器的内部算法会将当前运行的goroutine
让出,防止某个goroutine
长时间占用逻辑处理器。由于示例程序中两个goroutine
的执行时间都很短,在为引起调度器调度之前已经执行完。不过,程序也可以使用runtime.Gosched()
来将当前在逻辑处理器上运行的goruntine
让出,让另一个goruntine
得到执行:
//example2.go
package main
import (
"runtime"
"sync"
"fmt"
)
var (
wg sync.WaitGroup
)
func main() {
//分配一个逻辑处理器P给调度器使用
runtime.GOMAXPROCS(1)
//在这里,wg用于等待程序完成,计数器加2,表示要等待两个goroutine
wg.Add(2)
//声明1个匿名函数,并创建一个goroutine
fmt.Printf("Begin Coroutines\\n")
go func() {
//在函数退出时,wg计数器减1
defer wg.Done()
//打印3次小写字母表
for count := 0; count < 3; count++ {
for char := \'a\'; char < \'a\'+26; char++ {
if char==\'k\'{
runtime.Gosched()
}
fmt.Printf("%c ", char)
}
}
}()
//声明1个匿名函数,并创建一个goroutine
go func() {
defer wg.Done()
//打印大写字母表3次
for count := 0; count < 3; count++ {
for char := \'A\'; char < \'A\'+26; char++ {
if char == \'K\'{
runtime.Gosched()
}
fmt.Printf("%c ", char)
}
}
}()
fmt.Printf("Waiting To Finish\\n")
//等待2个goroutine执行完毕
wg.Wait()
}
两个goroutine
在循环的字符为k/K的时候会让出逻辑处理器,程序的输出结果为:
Begin Coroutines
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 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 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
这里大小写字母果然是交替着输出了。不过从输出可以看到,第一次输出大写字母时遇到K没有让出逻辑处理器,这是什么原因还不是很清楚,调度器的调度机制?
二、处理竞争状态
并发程序避免不了的一个问题是对资源的同步访问。如果多个goroutine
在没有互相同步的情况下去访问同一个资源,并进行读写操作,这时goroutine
就处于竞争状态下:
//example3.go
package main
import (
"sync"
"runtime"
"fmt"
)
var (
//counter为访问的资源
counter int64
wg sync.WaitGroup
)
func addCount() {
defer wg.Done()
for count := 0; count < 2; count++ {
value := counter
//当前goroutine从线程退出
runtime.Gosched()
value++
counter=value
}
}
func main() {
wg.Add(2)
go addCount()
go addCount()
wg.Wait()
fmt.Printf("counter: %d\\n",counter)
}
//output:
counter: 4 或者counter: 2
这段程序中,goroutine
对counter
的读写操作没有进行同步,goroutine 1对counter的写结果可能被goroutine 2所覆盖。Go可通过如下方式来解决这个问题:
- 使用原子函数操作
- 使用互斥锁锁住临界区
- 使用通道
chan
1. 检测竞争状态
有时候竞争状态并不能一眼就看出来。Go 提供了一个非常有用的工具,用于检测竞争状态。使用方式是:
go build -race example4.go//用竞争
以上是关于《Go in action》读后记录:Go的并发与并行的主要内容,如果未能解决你的问题,请参考以下文章