共计 3800 个字符,预计需要花费 10 分钟才能阅读完成。
原文链接: Go 专栏|并发编程:goroutine,channel 和 sync
优雅的并发编程范式,欠缺的并发反对,杰出的并发性能是 Go 语言区别于其余语言的一大特色。
在当今这个多核时代,并发编程的意义显而易见。应用 Go 开发并发程序,操作起来非常简单,语言级别提供关键字 go
用于启动协程,并且在同一台机器上能够启动成千上万个协程。
上面就来具体介绍。
goroutine
Go 语言的并发执行体称为 goroutine,应用关键词 go
来启动一个 goroutine。
go
关键词前面必须跟一个函数,能够是有名函数,也能够是无名函数,函数的返回值会被疏忽。
go
的执行是非阻塞的。
先来看一个例子:
package main
import (
"fmt"
"time"
)
func main() {go spinner(100 * time.Millisecond)
const n = 45
fibN := fib(n)
fmt.Printf("\rFibonacci(%d) = %d\n", n, fibN) // Fibonacci(45) = 1134903170
}
func spinner(delay time.Duration) {
for {
for _, r := range `-\|/` {fmt.Printf("\r%c", r)
time.Sleep(delay)
}
}
}
func fib(x int) int {
if x < 2 {return x}
return fib(x-1) + fib(x-2)
}
从执行后果来看,胜利计算出了斐波那契数列的值,阐明程序在 spinner
处并没有阻塞,而且 spinner
函数还始终在屏幕上打印提醒字符,阐明程序正在执行。
当计算完斐波那契数列的值,main
函数打印后果并退出,spinner
也跟着退出。
再来看一个例子,循环执行 10 次,打印两个数的和:
package main
import "fmt"
func Add(x, y int) {
z := x + y
fmt.Println(z)
}
func main() {
for i := 0; i < 10; i++ {go Add(i, i)
}
}
有问题了,屏幕上什么都没有,为什么呢?
这就要看 Go 程序的执行机制了。当一个程序启动时,只有一个 goroutine 来调用 main
函数,称为主 goroutine。新的 goroutine 通过 go
关键词创立,而后并发执行。当 main
函数返回时,不会期待其余 goroutine 执行完,而是间接暴力完结所有 goroutine。
那有没有方法解决呢?当然是有的,请往下看。
channel
个别写多过程程序时,都会遇到一个问题:过程间通信。常见的通信形式有信号,共享内存等。goroutine 之间的通信机制是通道 channel。
应用 make
创立通道:
ch := make(chan int) // ch 的类型是 chan int
通道反对三个次要操作:send
,receive
和 close
。
ch <- x // 发送
x = <-ch // 接管
<-ch // 接管,抛弃后果
close(ch) // 敞开
无缓冲 channel
make
函数承受两个参数,第二个参数是可选参数,示意通道容量。不传或者传 0 示意创立了一个无缓冲通道。
无缓冲通道上的发送操作将会阻塞,直到另一个 goroutine 在对应的通道上执行接管操作。相同,如果接管先执行,那么接管 goroutine 将会阻塞,直到另一个 goroutine 在对应通道上执行发送。
所以,无缓冲通道是一种同步通道。
上面咱们应用无缓冲通道把下面例子中呈现的问题解决一下。
package main
import "fmt"
func Add(x, y int, ch chan int) {
z := x + y
ch <- z
}
func main() {ch := make(chan int)
for i := 0; i < 10; i++ {go Add(i, i, ch)
}
for i := 0; i < 10; i++ {fmt.Println(<-ch)
}
}
能够失常输入后果。
主 goroutine 会阻塞,直到读取到通道中的值,程序继续执行,最初退出。
缓冲 channel
创立一个容量是 5 的缓冲通道:
ch := make(chan int, 5)
缓冲通道的发送操作在通道尾部插入一个元素,接管操作从通道的头部移除一个元素。如果通道满了,发送会阻塞,直到另一个 goroutine 执行接管。相同,如果通道是空的,接管会阻塞,直到另一个 goroutine 执行发送。
有没有感觉,其实缓冲通道和队列一样,把操作都解耦了。
单向 channel
类型 chan<- int
是一个只能发送的通道,类型 <-chan int
是一个只能接管的通道。
任何双向通道都能够用作单向通道,但反过来不行。
还有一点须要留神,close
只能用在发送通道上,如果用在接管通道会报错。
看一个单向通道的例子:
package main
import "fmt"
func counter(out chan<- int) {
for x := 0; x < 10; x++ {out <- x}
close(out)
}
func squarer(out chan<- int, in <-chan int) {
for v := range in {out <- v * v}
close(out)
}
func printer(in <-chan int) {
for v := range in {fmt.Println(v)
}
}
func main() {n := make(chan int)
s := make(chan int)
go counter(n)
go squarer(s, n)
printer(s)
}
sync
sync 包提供了两种锁类型:sync.Mutex
和 sync.RWMutex
,前者是互斥锁,后者是读写锁。
当一个 goroutine 获取了 Mutex
后,其余 goroutine 不论读写,只能期待,直到锁被开释。
package main
import (
"fmt"
"sync"
"time"
)
func main() {
var mutex sync.Mutex
wg := sync.WaitGroup{}
// 主 goroutine 先获取锁
fmt.Println("Locking (G0)")
mutex.Lock()
fmt.Println("locked (G0)")
wg.Add(3)
for i := 1; i < 4; i++ {go func(i int) {
// 因为主 goroutine 先获取锁,程序开始 5 秒会阻塞在这里
fmt.Printf("Locking (G%d)\n", i)
mutex.Lock()
fmt.Printf("locked (G%d)\n", i)
time.Sleep(time.Second * 2)
mutex.Unlock()
fmt.Printf("unlocked (G%d)\n", i)
wg.Done()}(i)
}
// 主 goroutine 5 秒后开释锁
time.Sleep(time.Second * 5)
fmt.Println("ready unlock (G0)")
mutex.Unlock()
fmt.Println("unlocked (G0)")
wg.Wait()}
RWMutex
属于经典的单写多读模型,当读锁被占用时,会阻止写,但不阻止读。而写锁会阻止写和读。
package main
import (
"fmt"
"sync"
"time"
)
func main() {
var rwMutex sync.RWMutex
wg := sync.WaitGroup{}
Data := 0
wg.Add(20)
for i := 0; i < 10; i++ {go func(t int) {
// 第一次运行后,写解锁。// 循环到第二次时,读锁定后,goroutine 没有阻塞,同时读胜利。fmt.Println("Locking")
rwMutex.RLock()
defer rwMutex.RUnlock()
fmt.Printf("Read data: %v\n", Data)
wg.Done()
time.Sleep(2 * time.Second)
}(i)
go func(t int) {
// 写锁定下是须要解锁后能力写的
rwMutex.Lock()
defer rwMutex.Unlock()
Data += t
fmt.Printf("Write Data: %v %d \n", Data, t)
wg.Done()
time.Sleep(2 * time.Second)
}(i)
}
wg.Wait()}
总结
并发编程算是 Go 的特色,也是外围性能之一了,波及的知识点其实是十分多的,本文也只是起到一个抛砖引玉的作用而已。
本文开始介绍了 goroutine 的简略用法,而后引出了通道的概念。
通道有三种:
- 无缓冲通道
- 缓冲通道
- 单向通道
最初介绍了 Go 中的锁机制,别离是 sync 包提供的 sync.Mutex
(互斥锁)和 sync.RWMutex
(读写锁)。
goroutine 博大精深,前面的坑还是要缓缓踩的。
文章中的脑图和源码都上传到了 GitHub,有须要的同学可自行下载。
地址: https://github.com/yongxinz/g…
Go 专栏文章列表:
- Go 专栏|开发环境搭建以及开发工具 VS Code 配置
- Go 专栏|变量和常量的申明与赋值
- Go 专栏|根底数据类型:整数、浮点数、复数、布尔值和字符串
- Go 专栏|复合数据类型:数组和切片 slice
- Go 专栏|复合数据类型:字典 map 和 构造体 struct
- Go 专栏|流程管制,一网打尽
- Go 专栏|函数那些事
- Go 专栏|错误处理:defer,panic 和 recover
- Go 专栏|说说办法
- Go 专栏|接口 interface