最近在学go语言的多线程,作为一门云时代的语言,并发处理应该是面试和应用中必不可少的一部分,强制自己写个笔记,好好整理一下,本人纯小白,只是课堂知识的自我消化,如果能帮助到大家,求之不得。
一、不加锁
多线程中使用睡眠函数不优雅,直接用sync.WaitGroup保证一个goroutine刚退出就可以继续执行,不需要自己猜需要sleep多久。
package main
import (
"fmt"
"sync"
)
var wg sync.WaitGroup
func main() {
wg.Add(10) // 启动一个goroutine就登记+1,启动十个就+10
var count=0
for i:=0;i<10;i++{
go func(){
defer wg.Done() // goroutine结束就登记-1
for j:=0;j<100000;j++{
count++
}
}()
}
wg.Wait() // 等待所有登记的goroutine都结束
fmt.Print(count)
}
启动十个goroutine对count自增10w次,理想状况为100w,但由于没有锁,会出现实际情况远远小于并且不相等的情况。
为什么会出现这样的结果?因为自增并不是一个原子操作,很可能几个goroutine同时读到同一个数,自增,又将同样的数写了回去,汇编代码如下:
MOVQ "".count(SB), AX
LEAQ 1(AX), CX
MOVQ CX, "".count(SB)
二、互斥锁
(1)直接使用锁
共享资源是count变量,临界区是count++,临界区之前加锁,使其他goroutine在临界区阻塞,离开临界区解锁,就可以解决这个data race的问题。go语言是通过sync.Mutex实现这一功能。
package main
import (
"fmt"
"sync"
)
var wg sync.WaitGroup
func main() {
var mu sync.Mutex //多
wg.Add(10)
var count = 0
for i := 0; i < 10; i++ {
go func() {
defer wg.Done()
for j := 0; j < 100000; j++ {
mu.Lock() //多
count++
mu.Unlock() //多
}
}()
}
wg.Wait()
fmt.Print(count)
}
为方便观察,多出的三行已经加上注释,使用很简单,执行,也干脆利落的输出了100w。
(2)嵌入字段方式使用锁
把Mutex嵌入struct,可以直接在这个struct上使用Lock/Unlock。
package main
import (
"fmt"
"sync"
)
var wg sync.WaitGroup
type Counter struct{
sync.Mutex
Count uint64
}
func main() {
var counter Counter
wg.Add(10)
for i := 0; i < 10; i++ {
go func() {
defer wg.Done()
for j := 0; j < 100000; j++ {
counter.Lock()
counter.Count++
counter.Unlock()
}
}()
}
wg.Wait()
fmt.Print(counter.Count)
}
结果不必多说,也是干脆的100w。(祝大家的年薪都超过这个数)
(3)把加/解锁封装成方法
package main
import (
"fmt"
"sync"
)
var wg sync.WaitGroup
type Counter struct{
mu sync.Mutex
count uint64
}
func (c *Counter)Incr(){
c.mu.Lock()
c.count++
c.mu.Unlock()
}
func (c*Counter)Count()uint64{
c.mu.Lock()
defer c.mu.Unlock()
return c.count
}
func main() {
var counter Counter
wg.Add(10) // 启动一个goroutine就登记+1,启动十个就+10
for i := 0; i < 10; i++ {
go func() {
defer wg.Done() // goroutine结束就登记-1
for j := 0; j < 100000; j++ {
counter.Incr()
}
}()
}
wg.Wait() // 等待所有登记的goroutine都结束
fmt.Print(counter.Count())
}
加锁操作和解锁操作被封装成了方法,顺便复习一下方法的使用。