这是我参与「第五届青训营 」伴学笔记创作活动的第 2 天
Go 语言进阶与依赖管理
3 并发编程
并发:多线程程序在一个核的cpu上运行
并行:多线程程序在多个核的cpu上运行
Go 可以充分发挥多核优势,高效运行
3.1 goroutine
goroutine 是通过 Go 的 runtime 管理的一个线程管理器。
goroutine 通过 go 关键字实现了,其实就是一个普通的函数。
携程:用户态,轻量级线程,栈MB级别
线程:内核态,线程跑多个携程,KB级别
package main
import (
"fmt"
"runtime"
)
func say(s string) {
for i := 0; i < 5; i++ {
runtime.Gosched() //runtime.Gosched()表示让 CPU 把时间片让给别人,下次某个时候继续恢复执行该goroutine。
fmt.Println(s)
}
}
func main() {
go say("world") //开一个新的 Goroutines 执行
say("hello") //当前 Goroutines 执行
}
3.2 CSP(Communicating Sequential Processes)
设计上我们要遵循:不要通过共享来通信,而要通过通信来共享。
默认情况下,调度器仅使用单线程,也就是说只实现了并发。想要发挥多核处理器的并行,
需要在我们的程序中显示的调用 runtime.GOMAXPROCS(n) 告诉调度器同时使用多个线
程。GOMAXPROCS 设置了同时运行逻辑代码的系统线程的最大数量,并返回之前的设置。
如果 n < 1,不会改变当前设置。以后 Go 的新版本中调度得到改进后,这将被移除。
3.2.1 channels
goroutine 运行在相同的地址空间,因此访问共享内存必须做好同步。那么 goroutine 之间如
何进行数据的通信呢,Go 提供了一个很好的通信机制 channel。channel 可以与 Unix shell
中的双向管道做类比:可以通过它发送或者接收值。这些值只能是特定的类型:channel 类
型。定义一个 channel 时,也需要定义发送到 channel 的值的类型。注意,必须使用 make
创建 channel:
ci := make(chan int)
cs := make(chan string)
cf := make(chan interface{})
channel 通过操作符<-来接收和发送数据
ch <- v // 发送 v 到 channel ch.
ch <- v // 发送 v 到 channel ch.
v := <-ch // 从 ch 中接收数据,并赋值给 v
我们把这些应用到我们的例子中来:
package main
import "fmt"
func sum(a []int, c chan int) {
sum := 0
for _, v := range a {
sum += v
}
c <- sum // send sum to c
}
func main() {
a := []int{7, 2, 8, -9, 4, 0}
c := make(chan int)
go sum(a[:len(a)/2], c)
go sum(a[len(a)/2:], c)
x, y := <-c, <-c // receive from c
fmt.Println(x, y, x+y)
}
默认情况下,channel 接收和发送数据都是阻塞的,除非另一端已经准备好,这样就使得
Goroutines 同步变的更加的简单,而不需要显式的 lock。所谓阻塞,也就是如果读取
(value := <-ch)它将会被阻塞,直到有数据接收。其次,任何发送(ch<-5)将会被阻塞,
直到数据被读出。无缓冲 channel 是在多个 goroutine 之间同步很棒的工具。
3.2.2 Buffered Channels(缓冲通道)
上面我们介绍了默认的非缓存类型的 channel,不过 Go 也允许指定 channel 的缓冲大小,
很简单,就是 channel 可以存储多少元素。ch:= make(chan bool, 4),创建了可以存储 4 个
元素的 bool 型 channel。在这个 channel 中,前 4 个元素可以无阻塞的写入。当写入第 5 个
元素时,代码将会阻塞,直到其他 goroutine 从 channel 中读取一些元素,腾出空间。
ch := make(chan type, value)
value == 0 ! 无缓冲(阻塞)
value > 0 ! 缓冲(非阻塞,直到 value 个元素)
3.2.3 并发安全LOCK
保证并发的安全,临界区问题
var (
x int64
lock sync.Mutex
)
func addWithLock() {
for i := 0; i < 2000; i++ {
lock.Lock()
x += 1
lock.Unlock()
}
}
3.2.4 Range 和 Close
可以通过 range,像操作 slice 或者 map 一样操作缓存类型的 channel
package main
import (
"fmt"
)
func fibonacci(n int, c chan int) {
x, y := 1, 1
for i := 0; i < n; i++ {
c <- x
x, y = y, x+y
}
close(c)//生产者通过关键字 close 函数关闭 channel,关闭channel 之后就无法再发送任何数据了
}
func main() {
c := make(chan int, 10)
go fibonacci(cap(c), c)
for i := range c {
fmt.Println(i)
}
}
在消费方可以通过语法 v, ok := <-ch 测试 channel是否被关闭。如果 ok 返回 false,那么说明 channel 已经没有任何数据并且已经被关闭。
记住应该在生产者的地方关闭 channel,而不是消费的地方去关闭它,这样容易引起 panic另外记住一点的就是 channel 不像文件之类的,不需要经常去关闭,只有当你确实没有任何发送数据了,或者你想显式的结束 range 循环之类的。
3.2.5 Select
我们上面介绍的都是只有一个 channel 的情况,那么如果存在多个 channel 的时候,我们
该如何操作呢,Go 里面提供了一个关键字 select,通过 select 可以监听 channel 上的数据
流动。
select 默认是阻塞的,只有当监听的 channel 中有发送或接收可以进行时才会运行,当多
个 channel 都准备好的时候,select 是随机的选择一个执行的。
package main
import "fmt"
func fibonacci(c, quit chan int) {
x, y := 1, 1
for {
select {
case c <- x:
x, y = y, x+y
case <-quit:
fmt.Println("quit")
return
}
}
}
func main() {
c := make(chan int)
quit := make(chan int)
go func() {
for i := 0; i < 10; i++ {
fmt.Println(<-c)
}
quit <- 0
}()
fibonacci(c, quit)
}
在 select 里面还有 default 语法,select 其实就是类似 switch 的功能,default 就是当监听的
channel 都没有准备好的时候,默认执行的(select 不再阻塞等待 channel)
select {
case i := <-c:
// use i
default:
// 当 c 阻塞的时候执行这里
}
3.2.6 超时
package main
import "time"
func main() {
c := make(chan int)
o := make(chan bool)
go func() {
for {
select {
case v := <-c:
println(v)
case <-time.After(5 * time.Second):
println("timeout")
o <- true
break
}
}
}()
<-o
}
3.2.7 runtime goroutine
runtime 包中有几个处理 goroutine 的函数:
• Goexit
退出当前执行的 goroutine,但是 defer 函数还会继续调用
• Gosched
让出当前 goroutine 的执行权限,调度器安排其他等待的任务运行,并在下次某个时候从
该位置恢复执行。
• NumCPU
返回 CPU 核数量
• NumGoroutine
返回正在执⾏行和排队的任务总数
• GOMAXPROCS用来设置可以运行的 CPU 核数
未完~