基于共享变量的并发
一、 竞争条件
多个 goroutine 并发访问同一变量时,如果存在对该变量的写操作。就会发生竞争。 go 的口头禅:不要使用共享数据来通信;使用通信来共享数据。
- 避免竞争方法
方法一、不要去写共享变量 方法二、避免从多个goroutine访问变量 方法三、使用互斥锁
二、 sync.Mutex互斥锁
- 二元信号量
一个只能为0和1信号量叫做二元信号量
var (
sema = make(chan struct{}, 1) // a binary semaphore guarding balance
balance int
)
func Deposit(amount int) {
sema <- struct{}{} // acquire token
balance = balance + amount
<-sema // release token
}
func Balance() int {
sema <- struct{}{} // acquire token
b := balance
<-sema // release token
return b
}
- sync 包中的 Mutex 类似二元信号量
import "sync"
var (
mu sync.Mutex
balance int
)
func Deposit(amount int) {
mu.Lock()
//defer mu.Unlock()
//临界区开始
balance = balance + amount
//临界区结束
mu.Unlock()
}
func Balance() int {
mu.Lock()
defer mu.Unlock()
return balance
}
- 没有重入锁
go 中没有重入锁,一旦锁被Lock,其它的goroutine将不能获取锁 当需要重入锁时,可利用封装方法,将导致重入锁的代码分离,单独封装后使用
// NOTE: incorrect!
var (
mu sync.Mutex
balance int
)
func Withdraw(amount int) bool {
mu.Lock()
defer mu.Unlock()
Deposit(-amount)
if Balance() < 0 {
Deposit(amount)
return false // insufficient funds
}
return true
}
// right
func Withdraw(amount int) bool {
mu.Lock()
defer mu.Unlock()
deposit(-amount)
if balance < 0 {
deposit(amount)
return false // insufficient funds
}
return true
}
func Deposit(amount int) {
mu.Lock()
defer mu.Unlock()
deposit(amount)
}
func Balance() int {
mu.Lock()
defer mu.Unlock()
return balance
}
// This function requires that the lock be held.
func deposit(amount int) { balance += amount }
三、 sync.RWMutex读写锁
- 多读单写锁
多个只读操作并行执行,但写操作会完全互斥, 这种锁叫作“多读单写”锁((multiple readers, single writer lock)
var (
mu sync.RWMutex
balance int
)
func Withdraw(amount int) bool {
mu.Lock()
defer mu.Unlock()
deposit(-amount)
if balance < 0 {
deposit(amount)
return false // insufficient funds
}
return true
}
func Deposit(amount int) {
mu.Lock()
defer mu.Unlock()
deposit(amount)
}
func Balance() int {
mu.RLock() //这里调用的读锁,其它保持不变
defer mu.RUnlock()
return balance
}
// This function requires that the lock be held.
func deposit(amount int) { balance += amount }
//sync.RWMutex 一般比 sync.Mutex 要慢一些
四、 内存同步
尽量将变量限定在goroutine内部;如果是多个goroutine都需要访问的变量,使用互斥条件来访问。
五、 sync.Once初始化
- 使用读写锁的时候,读并发,写互斥
var mu sync.RWMutex // guards icons
var icons map[string]image.Image
// Concurrency-safe.
func Icon(name string) image.Image {
mu.RLock()
if icons != nil {
icon := icons[name]
mu.RUnlock()
return icon
}
mu.RUnlock()
// acquire an exclusive lock
mu.Lock()
if icons == nil { // NOTE: must recheck for nil
loadIcons()
}
icon := icons[name]
mu.Unlock()
return icon
}
- 使用 sync.Once 解决一次性初始化问题
var loadIconsOnce sync.Once
var icons map[string]image.Image
// Concurrency-safe.
func Icon(name string) image.Image {
loadIconsOnce.Do(loadIcons)
return icons[name]
}
五、 并发非阻塞缓存
- 锁和通道形式
package memo
import "sync"
// Func is the type of the function to memoize.
type Func func(string) (interface{}, error)
type result struct {
value interface{}
err error
}
//!+
type entry struct {
res result
ready chan struct{} // closed when res is ready
}
func New(f Func) *Memo {
return &Memo{f: f, cache: make(map[string]*entry)}
}
type Memo struct {
f Func
mu sync.Mutex // guards cache
cache map[string]*entry
}
func (memo *Memo) Get(key string) (value interface{}, err error) {
memo.mu.Lock()
e := memo.cache[key]
if e == nil {
// This is the first request for this key.
// This goroutine becomes responsible for computing
// the value and broadcasting the ready condition.
e = &entry{ready: make(chan struct{})}
memo.cache[key] = e
memo.mu.Unlock()
e.res.value, e.res.err = memo.f(key)
close(e.ready) // broadcast ready condition
} else {
// This is a repeat request for this key.
memo.mu.Unlock()
<-e.ready // wait for ready condition
}
return e.res.value, e.res.err
}
- monitor goroutine
package memo
//!+Func
// Func is the type of the function to memoize.
type Func func(key string) (interface{}, error)
// A result is the result of calling a Func.
type result struct {
value interface{}
err error
}
type entry struct {
res result
ready chan struct{} // closed when res is ready
}
//!-Func
//!+get
// A request is a message requesting that the Func be applied to key.
type request struct {
key string
response chan<- result // the client wants a single result
}
type Memo struct{ requests chan request }
// New returns a memoization of f. Clients must subsequently call Close.
func New(f Func) *Memo {
memo := &Memo{requests: make(chan request)}
go memo.server(f)
return memo
}
func (memo *Memo) Get(key string) (interface{}, error) {
response := make(chan result)
memo.requests <- request{key, response}
res := <-response
return res.value, res.err
}
func (memo *Memo) Close() { close(memo.requests) }
//!-get
//!+monitor
func (memo *Memo) server(f Func) {
cache := make(map[string]*entry)
for req := range memo.requests {
e := cache[req.key]
if e == nil {
// This is the first request for this key.
e = &entry{ready: make(chan struct{})}
cache[req.key] = e
go e.call(f, req.key) // call f(key)
}
go e.deliver(req.response)
}
}
func (e *entry) call(f Func, key string) {
// Evaluate the function.
e.res.value, e.res.err = f(key)
// Broadcast the ready condition.
close(e.ready)
}
func (e *entry) deliver(response chan<- result) {
// Wait for the ready condition.
<-e.ready
// Send the result to the client.
response <- e.res
}
//!-monitor
- 结论
上面的两种方案 (锁与通信) 并不好说特定情境下哪种更好,不过了解他们还是有价值的。 有时候从一种方式切换到另一种可以使你的代码更为简洁。
六、 Goroutines 和线程
- 每个 OS 线程都有一个固定大小的内存块(一般会是 2 MB)来做栈;Goroutine 只需要 2 KB做栈开始
- OS 线程栈是固定的;Goroutine 栈是动态伸缩的,最大可至 1 GB
- Go 调度器工作更快,更简单
- GOMAXPROCS 决定有多少个线程同时执行 GO 代码
- 线程有 ID 号,Groutine 没有 ID 号