互斥锁

对于任一共享资源,同一时间保证只有一个操作者,这种方法称为互斥机制

关键字Mutex表示互斥锁类型,它的Lock方法用于获取锁,Unlock方法用于释放锁。在LockUnlock之间的代码,可以读取和修改共享资源,这部分区域称为临界区

错误的并发操作

先来看一个错误的示例。

在Map小节中讲到,Map不是并发安全的,也就是说,如果在多个线程中,同时对一个 Map 进行读写,会报错。现在来验证一下, 通过启动100 个 goroutine来模拟并发调用,每个 goroutine 都对 Map 的 key 进行设置。

packagemain
import"sync"
funcmain(){
m:=make(map[int]bool)
varwgsync.WaitGroup
forj:=0;j<100;j++{
wg.Add(1)
gofunc(keyint){
deferfunc(){
wg.Done()
}()
m[key]=true//对Map进行并发写入
}(j)
}
wg.Wait()
}
//$gorunmain.go
//输出如下,报错信息
/**
fatalerror:concurrentmapwrites
fatalerror:concurrentmapwrites
goroutine104[running]:
main.main.func1(0x0?)
/home/codes/Go-examples-for-beginners/main.go:18+0x66
createdbymain.main
/home/codes/Go-examples-for-beginners/main.go:13+0x45
goroutine1[semacquire]:
sync.runtime_Semacquire(0xc0000112c0?)
/usr/local/go/src/runtime/sema.go:62+0x25
sync.(*WaitGroup).Wait(0x60?)
/usr/local/go/src/sync/waitgroup.go:139+0x52
main.main()
/home/codes/Go-examples-for-beginners/main.go:22+0x105
...
...
...
*/

通过输出信息fatal error: concurrent map writes可以看到,并发写入 Map 确实会报错。

正确的并发操作

Map 并发写入如何正确地实现呢?

一种简单的方案是在并发临界区域 (也就是设置 Map key 的地方) 进行加互斥锁操作,互斥锁保证了同一时刻 只有一个 goroutine 获得锁,其他 goroutine 全部处于等待状态,这样就把并发写入变成了串行写入, 从而消除了报错问题。

packagemain
import(
"fmt"
"sync"
)
funcmain(){
varmusync.Mutex
m:=make(map[int]bool)
varwgsync.WaitGroup
forj:=0;j<100;j++{
wg.Add(1)
gofunc(keyint){
deferfunc(){
wg.Done()
}()
mu.Lock()//写入前加锁
m[key]=true//对Map进行并发写入
mu.Unlock()//写入完成解锁
}(j)
}
wg.Wait()
fmt.Printf("Mapsize=%d\n",len(m))
}
//$gorunmain.go
//输出如下
/**
Mapsize=100
*/

超时控制

利用channel (通道)time.After()方法实现超时控制。

例子

packagemain
import(
"fmt"
"time"
)
funcmain(){
ch:=make(chanbool)
gofunc(){
deferfunc(){
ch<-true
}()
time.Sleep(2*time.Second)//模拟超时操作
}()
select{
case<-ch:
fmt.Println("ok")
case<-time.After(time.Second):
fmt.Println("timeout!")
}
}
//$gorunmain.go
//输出如下
/**
timeout!
*/

定时器

调用time.NewTicker方法即可。

例子

packagemain
import(
"fmt"
"time"
)
funcmain(){
ticker:=time.NewTicker(time.Second)
deferticker.Stop()
done:=make(chanbool)
gofunc(){
time.Sleep(5*time.Second)//模拟耗时操作
done<-true
}()
for{
select{
case<-done:
fmt.Println("Done!")
return
case<-ticker.C:
fmt.Println(time.Now().Format("2006-01-0215:04:05"))
}
}
}
//$gorunmain.go
//输出如下,你的输出可能和这里的不一样
/**
2021-01-0315:40:21
2021-01-0315:40:22
2021-01-0315:40:23
2021-01-0315:40:24
2021-01-0315:40:25
Done!
*/

扩展阅读

  1. 1.互斥锁 - 维基百科 (https://zh.wikipedia.org/wiki/互斥锁)

  2. 2.临界区 - 百度百科 (https://baike.baidu.com/item/临界区/8942134)

联系我

Go 快速入门指南 - 互斥锁和定时器

发表回复