刚开始接触Go语言,跟的孟凡杰老师的课程进行学习,仅以博客作为记录,如有不对欢迎支出。
锁
Go语言不仅仅提供基于CSP通讯模型,也支持基于共享内存的多线程数据访问。Go中提供了Sync的包提供的基本原语,常用的有如下几种:
- Mutex互斥锁
- RWMutex读写分离锁
- WaitGroup等待返回goroutine
- Once单实例
- Cond在满足一定条件时候唤醒goroutine
Mutex互斥锁
在读写的时候进行互斥,防止出现同时读和同时写的情况。
package main
import (
"sync"
"time"
)
func main() {
//unsafeWrite()
safeWrite()
time.Sleep(time.Second)
}
func unsafeWrite() {
conflictMap := map[int]int{}
for i := 0; i < 100; i++ {
go func() {
conflictMap[1] = i
}()
}
}
type SafeMap struct {
safeMap map[int]int
sync.Mutex
}
func safeWrite() {
s := SafeMap{
safeMap: map[int]int{},
Mutex: sync.Mutex{},
}
for i := 0; i < 100; i++ {
go func() {
s.Write(1, 1)
}()
}
}
func (s *SafeMap) Read(k int) (int, bool) {
s.Lock()
defer s.Unlock()
result, ok := s.safeMap[k]
return result, ok
}
func (s *SafeMap) Write(k, v int) {
s.Lock()
defer s.Unlock()
s.safeMap[k] = v
}
通过Lock和Unlock的方式来实现互斥的操作
RWMutex读写分离锁
要求多协程在执行的时候,读操作不会影响写,所以读不互斥,但是读写之间互斥,以及写之间互斥。
package main
import (
"fmt"
"sync"
"time"
)
func main() {
go rLock()
go wLock()
go lock()
time.Sleep(5 * time.Second)
}
func lock() {
lock := sync.Mutex{}
for i := 0; i < 3; i++ {
lock.Lock()
defer lock.Unlock()
fmt.Println("lock:", i)
}
}
func rLock() {
lock := sync.RWMutex{}
for i := 0; i < 3; i++ {
lock.RLock()
defer lock.RUnlock()
fmt.Println("rLock:", i)
}
}
func wLock() {
lock := sync.RWMutex{}
for i := 0; i < 3; i++ {
lock.Lock()
defer lock.Unlock()
fmt.Println("wLock:", i)
}
}
通过观察输出结果可以发现,R锁可以继续执行,因为之间不互斥,但是W锁不能继续执行,因为写之间是互斥的。
WaitGroup等待返回goroutine
在协程内部的goroutine在执行后都会Done掉,主协程会等待goroutine,当所有goroutine都完成了才会完整退出,这从时间上完美解决了,不知如何设置定时的问题。
ackage main
import (
"fmt"
"sync"
"time"
)
func main() {
waitByWG()
}
func waitByWG() {
wg := sync.WaitGroup{}
wg.Add(100)
for i := 0; i < 100; i++ {
go func(i int) {
fmt.Println(i)
wg.Done()
}(i)
}
wg.Wait()
}
Once单实例
同一个代码块可能被多次调用操作,但要求它是单实例,指实例化一次,通过Once即可完成操作。通过Once.Do的操作来执行。
package main
import (
"fmt"
"sync"
)
type SliceNum []int
func NewSlice() SliceNum {
return make(SliceNum, 0)
}
func (s *SliceNum) Add(elem int) *SliceNum {
*s = append(*s, elem)
fmt.Println("add", elem)
fmt.Println("add SliceNum end", s)
return s
}
func main() {
var once sync.Once
s := NewSlice()
// 看源代码理解once的行为
once.Do(func() {
s.Add(16)
})
once.Do(func() {
s.Add(16)
})
once.Do(func() {
s.Add(16)
})
}
其中Once.Do的源码如下
if atomic.LoadUint32(&o.done) == 0 {
// Outlined slow-path to allow inlining of the fast-path.
o.doSlow(f)
}
func (o *Once) doSlow(f func()) {
o.m.Lock()
defer o.m.Unlock()
if o.done == 0 {
defer atomic.StoreUint32(&o.done, 1)
f()
}
}
通过一个doSlow的操作来实现这个逻辑,当每次这个done为0的时候都会执行完f()回调函数后将其值设置为1,保证了单实例。
Cond在满足一定条件时候唤醒goroutine
采用生产者消费者的问题来补充理解,当容量为1时候,规定了只能生产1和消费1,则在生产时候,消费只能执行等待,当生产完后则进行协程的唤醒,可以执行消费。在GO中使用的是Signal和Broadcast两个标签来执行等待的通知,唤醒goroutine。
package main
import (
"fmt"
"sync"
"time"
)
type Queue struct {
queue []string
cond *sync.Cond
}
func main() {
q := Queue{
queue: []string{},
cond: sync.NewCond(&sync.Mutex{}),
}
go func() {
for {
q.Enqueue("a")
time.Sleep(time.Second * 2)
}
}()
for {
q.Dequeue()
time.Sleep(time.Second)
}
}
func (q *Queue) Enqueue(item string) {
q.cond.L.Lock()
defer q.cond.L.Unlock()
q.queue = append(q.queue, item)
fmt.Printf("putting %s to queue, notify all\n", item)
q.cond.Broadcast()
}
func (q *Queue) Dequeue() string {
q.cond.L.Lock()
defer q.cond.L.Unlock()
for len(q.queue) == 0 {
fmt.Println("no data available, wait")
q.cond.Wait()
}
result := q.queue[0]
q.queue = q.queue[1:]
return result
}