并发编程
并发在图中的解释是两队人排队接咖啡,两队切换。
并行是两个咖啡机,两队人同时接咖啡
goroutine
启动goroutine只要在前面调用函数前加go关键字即可
一个goroutine必定对应一个函数,可以创建多个goroutine执行相同的函数
通过
runtime.GOMAXPROCS(n)
函数设置当前程序并发时占用的CPU逻辑核心数
并发安全和锁
package main
import (
"fmt"
"sync"
"time"
)
func worker(id int, wg *sync.WaitGroup){
defer wg.Done()
fmt.Printf("Worker %d starting
",id)
time.Sleep(time.Second)
fmt.Printf("Worker %d done
",id)
}
func DoSomething(doonce *sync.Once){
doonce.Do(func(){
fmt.Println("Run once - first time, loading...")
})
fmt.Println("Run this every time")
}
// SafeCounter 的并发使用是安全的。
type SafeCounter struct {
v map[string]int
mux sync.Mutex
}
// Inc 增加给定 key 的计数器的值。
func (c *SafeCounter) Inc(key string) {
c.mux.Lock()
defer c.mux.Unlock()
// Lock 之后同一时刻只有一个 goroutine 能访问 c.v
c.v[key]++
}
// Value 返回给定 key 的计数器的当前值。
func (c *SafeCounter) Value(key string) int {
c.mux.Lock()
// Lock 之后同一时刻只有一个 goroutine 能访问 c.v
defer c.mux.Unlock()
return c.v[key]
}
func main() {
// 计数器不能为负值
// WaitGroup对象不是引用类型
var wg sync.WaitGroup
for i := 1; i <= 3; i++{
wg.Add(1)
go worker(i, &wg)
}
wg.Wait()
// sync.Once控制函数只能被调用一次,不能多次重复调用
var doOnce sync.Once
DoSomething(&doOnce)
DoSomething(&doOnce)
// 互斥锁 Mutex
// 读写锁 RWMutex 读锁会阻止写但不会阻止读 RLock() RUnlock()释放
// 写锁就等同于Mutex
c := SafeCounter{v: make(map[string]int)}
for i := 0; i < 1000; i++ {
go c.Inc("somekey")
}
time.Sleep(time.Second)
fmt.Println(c.Value("somekey"))
// 条件变量 Cond
// 可以让一系列的 Goroutine 都在满足特定条件时被唤醒
}
原子操作
原子操作由内置的标准库
sync/atomic
提供
package main
import (
"fmt"
"sync"
"sync/atomic"
"time"
)
var x int64
var l sync.Mutex
var wg sync.WaitGroup
// 普通版加函数
func add() {
// x = x + 1
x++ // 等价于上面的操作
wg.Done()
}
// 互斥锁版加函数
func mutexAdd() {
l.Lock()
x++
l.Unlock()
wg.Done()
}
// 原子操作版加函数
func atomicAdd() {
atomic.AddInt64(&x, 1)
wg.Done()
}
func main() {
start := time.Now()
for i := 0; i < 10000; i++ {
wg.Add(1)
// go add() // 普通版add函数 不是并发安全的
// go mutexAdd() // 加锁版add函数 是并发安全的,但是加锁性能开销大
go atomicAdd() // 原子操作版add函数 是并发安全,性能优于加锁版
}
wg.Wait()
end := time.Now()
fmt.Println("原子操作版add函数 x=", x)
fmt.Println("原子操作版add函数", end.Sub(start))
}
Channel通道
Channel 是一种引用类型
var 变量 chan 元素类型
分类
无缓冲的Channel
发送与接受同时进行。如果没有Goroutine读取Channel(<-Channel),发送者(Channel<-x)会一直阻塞。
有缓冲的Channel
发送与接受并非同时进行。当队列为空,接受者阻塞;队列满,发送者阻塞。
worker pool(goroutine池)
指定启动的goroutine数量–worker pool模式,控制goroutine的数量,防止goroutine泄漏和暴涨。
select多路复用
使用select提高代码可读性
- 可处理一个或多个channel的发送/接收操作。
如果多个case同时满足,select会随机选择一个。
对于没有case的select{}会一直等待,可用于阻塞main函数。
package main
import (
"fmt"
)
func main() {
ch := make(chan int, 1)
for i := 0; i < 10; i++ {
select {
case x := <-ch:
fmt.Println(x)
case ch <- i:
}
}
}
参考
https://github.com/datawhalechina/go-talent/blob/master/12.并发编程.md