选择channel和互斥量
我们一般根据以下原则选择:
协程简介
golang的协程模型,是基于M->N
的绿色协程映射的。意思是有M
个编程语言级别的绿色线程,运行在N
个操作系统的线程上,操作系统的线程调度绿色线程,所以
M
≥
N
M\ge N
M≥N。之后,协程运行在这些绿色线程上,协程是不可抢占的,每个协程都有自己的时间片,但是不能被抢占。
goroutine
的代价非常小,一般来说,我们不需要考虑它的代价。需要注意,GC不回收协程,因此我们需要注意协程泄露的情况。
启动协程需要时间,Go语言经常利用闭包作为协程启动的基本单元,但是需要注意,闭包直接捕获外部循环变量时,可能存在不准确的情况,改进的做法是,使用把外部变量作为参数传入闭包:
package main
import (
"fmt"
"time"
)
func main() {
for i := 0; i < 10; i++ {
go func() {
fmt.Printf("%v ", i)
}()
}
time.Sleep(time.Second)
fmt.Println("\n============")
for i := 0; i < 10; i++ {
go func(n int) {
fmt.Printf("%v ", n)
}(i)
}
time.Sleep(time.Second)
}
/*
输出结果
10 10 10 10 10 10 2 10 10 10
============
0 1 2 6 5 7 3 8 4 9
*/
WaitGroup
该类型作为协程之间同步使用的,一般来说,我们需要等待一组并发协程归并时,需要借助这个工具。Wait()
是等待归并,Add(n)
是增加n
个数值,Done()
减少一个数据,当数据是0时,Wait()
停止阻塞。给出代码示例:
package main
import (
"fmt"
"sync"
"time"
)
func main() {
var wg sync.WaitGroup
N := 5
wg.Add(N) // 添加数据
fmt.Println("Start")
for i := 0; i < N; i++ {
go func() {
defer wg.Done() // 一般在defer中Done操作
fmt.Println("hello world")
time.Sleep(time.Second)
}()
}
fmt.Println("Waiting")
wg.Wait() // 这里阻塞
fmt.Println("Done !")
}
/*
Start
Waiting
hello world
hello world
hello world
hello world
hello world
Done !
*/
互斥锁和读写锁
互斥锁
互斥锁用于读写不确定的情况,而读写锁用于已知读写的情况。前者速度比后者慢,后者是有写锁时互斥的。
package main
import (
"fmt"
"sync"
)
func main() {
N := 0
var wg sync.WaitGroup
wg.Add(20)
for i := 0; i < 20; i++ {
go func() {
defer wg.Done()
for j := 0; j < 100; j++ {
N += 1
}
}()
}
wg.Wait()
fmt.Printf("Without lock, N=%d\n", N)
M := 0
wg.Add(20)
var lock sync.Mutex
for i := 0; i < 20; i++ {
go func(){
defer wg.Done()
for j := 0; j < 100; j++ {
lock.Lock()
M += 1
lock.Unlock()
}
}()
}
wg.Wait()
fmt.Printf("With lock, M=%d\n", M)
}
可能的结果输出:
Without lock, N=1802
With lock, M=2000
读写锁
效率更高的一种锁,给出代码示例:
package main
import (
"fmt"
"sync"
"time"
)
func main() {
var mu sync.RWMutex
var wg sync.WaitGroup
M := 0
wg.Add(3)
go func() {
defer wg.Done()
mu.RLock() # 读锁
fmt.Println("1", time.Now(), "M=", M)
time.Sleep(time.Second * 3)
mu.RUnlock()
}()
go func() {
defer wg.Done()
mu.RLock()
fmt.Println("2", time.Now(), "M=", M)
mu.RUnlock()
}()
go func() {
defer wg.Done()
mu.Lock()
M ++
mu.UnLock()
}
wg.Wait()
}
条件变量Cond
条件变量用于wait & signal
原语操作,给出代码示例:
package main
import (
"fmt"
"sync"
"time"
)
func main() {
c := sync.NewCond(&sync.Mutex{})
queue := make([]interface{}, 0, 10)
removeFromQueue := func(delay time.Duration) {
time.Sleep(delay)
c.L.Lock()
queue = queue[1:] // 利用切片移动,模拟出队
fmt.Println("Removed from queue")
c.L.Unlock()
c.Signal()
}
for i := 0; i < 10; i++ {
c.L.Lock()
for len(queue) == 2 {
c.Wait() // 超过两个立刻阻塞
}
fmt.Println("Adding to queue")
queue = append(queue, struct{}{})
go removeFromQueue(time.Second * 1)
c.L.Unlock()
}
}
再给出一个广播的例子,可以向一个按键点击行为注册不同的事件,借助sync.Cond
实现,代码如下:
package main
import (
"fmt"
"sync"
)
type Button struct { // 模拟按键
Clicked *sync.Cond
}
func main() {
button := Button{ Clicked: sync.NewCond(&sync.Mutex{}) }
subscribe := func(c *sync.Cond, fn func()) { // 订阅,收到c的消息就执行对应的函数
var goroutineRunning sync.WaitGroup
goroutineRunning.Add(1)
go func() {
goroutineRunning.Done()
c.L.Lock()
defer c.L.Unlock()
c.Wait()
fn()
}()
goroutineRunning.Wait()
}
// 订阅按键点击,响应不同的行为
var clickRegistered sync.WaitGroup
clickRegistered.Add(3) // 只是为了阻塞用的
subscribe(button.Clicked, func() {
fmt.Println("Maximizing window")
clickRegistered.Done()
})
subscribe(button.Clicked, func() {
fmt.Println("Displaying annoying dialog box !")
clickRegistered.Done()
})
subscribe(button.Clicked, func() {
fmt.Println("Mouse clicked")
clickRegistered.Done()
})
button.Clicked.Broadcast()
clickRegistered.Wait()
}
相对于channel
,该方式的优势在于,Boardcast()
方法可以调用多次,行为可以响应多次。注意一点,使用sync.Cond
时,最好在一个紧凑的范围内部,否则容易造成混乱。上述方式提供了一个消息注册的基本思路。
once
该方式保证函数只在全局调用一次,可以用作单例模式,给出代码示例:
package main
import (
"fmt"
"sync"
)
var once sync.Once // 注意需要声明
func foo() {
fmt.Println("foo")
}
func test() {
fmt.Println("test")
once.Do(foo) // 全局唯一执行
}
func main() {
for i := 0; i < 3; i++ {
test()
}
}
/*
输出结果:
test
foo
test
test
*/
注意一点,sync.Once
指计算调用Do
方法的次数,而不是多少次唯一调用Do
方法,举个例子:
package main
import (
"fmt"
"sync"
)
var once sync.Once
var count int
func Inc() { count++ }
func Dec() { count-- }
func main() {
once.Do(Inc)
once.Do(Dec)
fmt.Println(count)
}
上述代码输出1,因为once
变量的Do
只记录自身调用的次数,不是记录某个函数调用的次数!!
单例模式可以利用Do
方式唯一初始化。
Pool
这里是指资源池。首先,应该明确什么是资源池,为什么用资源池。某些资源申请需要花费较长的时间,为了可以快速获取这些资源,我们可以提前申请一定数量的资源放在“资源池”中,当需要资源时,可以从资源池中快速获取已经存在的资源;当使用完资源后,会把资源放回池中。
注意,一般来说资源池,不会限制申请资源的数量。比如资源池的容量是10,我们同时申请了15个资源,则有10个会快速从资源池中获取,有5个会重新创建。同样的,当放回的资源数超过10的时候,多的资源会被释放掉。
给出golang中资源池的使用方式:
package main
import (
"fmt"
"sync"
)
func main() {
var numCalsCreated int
calcPool := &sync.Pool{
New: func() interface{} {
numCalsCreated += 1
mem := make([]byte, 1024)
return &mem // 注意这里返回的地址
},
}
for i := 0; i < 4; i++ {
calcPool.Put(calcPool.New())
}
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.Println(numCalsCreated) // 8
}
虽然启动了1024 * 1024个协程,但是因为存在资源池,所以实际也就用到了8 * 1024字节的内存。
Map
并发安全的map
结构,这里不再赘述,直接参考文档即可。
https://golang.org/pkg/sync/#Map