Go 并发指南
这份指南基于书籍《Go 并发》和《Go 编程语言》中的一些示例构建。
竞态条件和数据竞赛
竞态条件发生在两个或多个操作必须按正确顺序执行时,但程序并未编写以保证维持此顺序。
数据竞赛发生在一个并发操作尝试读取变量时,而在某个不确定的时间点,另一个并发操作尝试写入同一个变量。主函数是主 goroutine。
func main() {
var data int
go func() {
data++
}()
if data == 0 {
fmt.Printf("the value is %d", data)
}
}
内存访问同步
sync 包包含了对低级内存访问同步最有用的并发原语。
临界区是您代码中可以访问共享内存的地方。
互斥锁 Mutex
互斥锁代表“互斥”,是保护程序临界区的一种方式。
type Counter struct {
mu sync.Mutex
value int
}
func (c *Counter) Increment() {
c.mu.Lock()
defer c.mu.Unlock()
c.value++
}
等待组 WaitGroup
调用以添加一组 goroutine
var wg sync.WaitGroup
for _, salutation := range []string{"hello", "greetings", "good day"} {
wg.Add(1)
go func(salutation string) {
defer wg.Done()
fmt.Println(salutation)
}(salutation)
}
wg.Wait()
读写互斥锁 RWMutex
更细粒度的内存控制,可能请求只读锁
producer := func(wg *sync.WaitGroup, l sync.Locker) {
defer wg.Done()
for i := 5; i > 0; i-- {
l.Lock()
l.Unlock()
time.Sleep(1)
}
}
observer := func(wg *sync.WaitGroup, l sync.Locker) {
defer wg.Done()
l.Lock()
defer l.Unlock()
}
test := func(count int, mutex, rwMutex sync.Locker) time.Duration {
var wg sync.WaitGroup
wg.Add(count+1)
beginTestTime := time.Now()
go producer(&wg, mutex)
for i := count; i > 0; i-- {
go observer(&wg, rwMutex)
}
wg.Wait()
return time.Since(beginTestTime)
}
tw := tabwriter.NewWriter(os.Stdout, 0, 1, 2, ' ', 0)
defer tw.Flush()
var m sync.RWMutex
fmt.Fprintf(tw, "Readers\tRWMutex\tMutex\n")
for i := 0; i < 20; i++ {
count := int(math.Pow(2, float64(i)))
fmt.Fprintf(
tw,
"%d\t%v\t%v\n",
count,
test(count, &m, m.RLocker()),
test(count, &m, &m),
)
}
条件变量 Cond
如果有一种方法可以让一个 goroutine 高效地休眠,直到被信号唤醒并检查其条件,那就好了。这正是 Cond 类型为我们提供的。
Cond 和 Broadcast 是提供通知阻塞在 Wait 调用上的 goroutines 条件已被触发的方法。
type Button struct {
Clicked *sync.Cond
}
func main() {
button := Button{
Clicked: sync.NewCond(&sync.Mutex{}),
}
// 在每个传递/注册的 goroutine 上运行
// 等待,直到确认该 goroutine 正在运行才退出
subscribe := func(c *sync.Cond, param string, fn func(s string)) {
var goroutineRunning sync.WaitGroup
goroutineRunning.Add(1)
go func(p string) {
goroutineRunning.Done()
c.L.Lock() // 临界区
defer c.L.Unlock()
fmt.Println("Registered and wait ... ")
c.Wait()
fn(p)
}(param)
goroutineRunning.Wait()
}
var clickRegistered sync.WaitGroup
for _, v := range []string{
"Maximizing window.",
"Displaying annoying dialog box!",
"Mouse clicked."} {
clickRegistered.Add(1)
subscribe(button.Clicked, v, func(s string) {
fmt.Println(s)
clickRegistered.Done()
})
}
button.Clicked.Broadcast()
clickRegistered.Wait()
}
Once
确保即使在多个 goroutine 中,也只执行一次
var count int
increment := func() {
count++
}
var once sync.Once
var increments sync.WaitGroup
increments.Add(100)
for i := 0; i < 100; i++ {
go func() {
defer increments.Done()
once.Do(increment)
}()
}
increments.Wait()
fmt.Printf("Count is %d\n", count)
连接池 Pool
管理连接池,数量
package main
import (
"fmt"
"sync"
)
func main() {
myPool := &sync.Pool{
New: func() interface{} {
fmt.Println("Creating new instance.")
return struct{}{}
},
}
// 如果没有实例,则 Get 调用在池中定义的 New 函数
myPool.Get()
instance := myPool.Get()
fmt.Println("instance", instance)
// 将之前检索到的实例放回池中,
// 这将可用实例的数量增加到一个
myPool.Put(instance)
// 当执行此调用时,我们将重用之前分配的实例
// 并将其放回池中
myPool.Get()
var numCalcsCreated int
calcPool := &sync.Pool{
New: func() interface{} {
fmt.Println("new calc pool")
numCalcsCreated += 1
mem := make([]byte, 1024)
return &mem
},
}
fmt.Println("calcPool.New", calcPool.New())
calcPool.Put(calcPool.New())
calcPool.Put(calcPool.New())
calcPool.Put(calcPool.New())
calcPool.Put(calcPool.New())
calcPool.Get()
const numWorkers = 1024 * 1024
var wg sync.WaitGroup
wg.Add(numWorkers)
for i := numWorkers; i > 0; i-- {
go func() {
defer wg.Done()
mem := calcPool.Get().(*[]byte)
defer calcPool.Put(mem)
// 假设对这段内存进行一些有趣但快速的操作。
}()
}
wg.Wait()
fmt.Printf("%d calculators were created.", numCalcsCreated)
}
死锁活锁和饥饿
死锁
死锁是一个程序,其中所有并发进程都在等待彼此。
package main
import (
"fmt"
"sync"
"time"
)
type value struct {
mu sync.Mutex
value int
}
func main() {
var wg