先说结论
1. golang提供了syscall包来实现文件/目录的加锁,解锁
2. syscall包属于文件锁,是比较底层的技术,并不能在所有操作系统上完全实现,linux上实现了,windows下面就没有
3. 加锁时调用syscall.Flock(fd,syscall.LOCK_EX),解锁时调用syscall.Flock(fd, syscall.LOCK_UN)
4. 加锁成功后,对加锁的文件fd进行Close()操作同样会释放锁,切记
代码实现
锁的定义如下内部两个变量:文件/目录的全路径名,文件对象
// 文件锁/目录锁
type DirLock struct {
dir string // 文件/目录的全路径名
f *os.File // 文件对象
}
加锁的实现
核心代码是 syscall.Flock(int(f.Fd()), LOCK_EX|syscall.LOCK_NB),注意其中的标记
LOCK_EX :加锁标记。只有一个进程能加锁成功,其他进程再尝试加锁时会阻塞,等同于我们常用的写锁
LOCK_NB :不阻塞标记。如果其他进程已加锁成功,自己去尝试加锁时就不再阻塞,而是直接返回错误
// 加锁
func (l *DirLock) Lock() error {
f, err := os.Open(l.dir)
if err != nil {
return err
}
l.f = ferr = syscall.Flock(int(f.Fd()), syscall.LOCK_EX|syscall.LOCK_NB)
if err != nil {
return err
}
return nil
}
解锁的实现
核心代码是 syscall.Flock(int(l.f.Fd()), syscall.LOCK_UN)
LOCK_UN :解锁标记。如果自己已经加锁成功,可以用此标记去解锁
// 解锁
func (l *DirLock) Unlock() error {
defer l.f.Close() // 关闭文件return syscall.Flock(int(l.f.Fd()), syscall.LOCK_UN) // LOCK_UN表解锁
}
实验
我们建立5个协程,每秒去尝试加锁一次,失败则1秒后重试,成功则持续2秒后解锁
核心代码如下
// 5个协程,都尝试对目录加锁,加锁失败的就重试,加锁成功的2秒后释放
for i := 0; i < 5; i++ {
wg.Add(1)go func(num int) {
dirLock := New(dir)
ticker := time.NewTicker(time.Second) // 定时器每秒尝试1次
for {
select {
case <-ticker.C:
{
err := dirLock.Lock() // 加锁尝试
if err != nil {
fmt.Printf("lock dir failed, goroutine num=%d, err=%s \n", num, err.Error())
continue
}
fmt.Println("lock dir succeed, goroutine num=", num)
goto end
}
}
}end:
time.Sleep(time.Second*2)
dirLock.Unlock() // 解锁
wg.Done()
}(i)
}
wg.Wait()
实验结果如下图
完整代码
package main
import (
"fmt"
"os"
"sync"
"syscall"
"time"
)
// 目录锁
type DirLock struct {
dir string // 目录的全路径名
f *os.File // 文件对象
}
func New(dir string) *DirLock {
return &DirLock{
dir: dir,
}
}
// 加锁
func (l *DirLock) Lock() error {
f, err := os.Open(l.dir)
if err != nil {
return err
}
l.f = f
err = syscall.Flock(int(f.Fd()), syscall.LOCK_EX|syscall.LOCK_NB)
if err != nil {
return err
}
return nil
}
// 释放锁
func (l *DirLock) Unlock() error {
defer l.f.Close() // 其实不执行Close()也会释放目录锁
return syscall.Flock(int(l.f.Fd()), syscall.LOCK_UN)
}
func main() {
dir, _ := os.Getwd()
wg := sync.WaitGroup{}
// 5个协程,都尝试对目录加锁,加锁失败的就重试,加锁成功的2秒后释放
for i := 0; i < 5; i++ {
wg.Add(1)
go func(num int) {
dirLock := New(dir)
ticker := time.NewTicker(time.Second) // 定时器每秒尝试1次
for {
select {
case <-ticker.C:
{
err := dirLock.Lock() // 加锁尝试
if err != nil {
fmt.Printf("lock dir failed, goroutine num=%d, err=%s \n", num, err.Error())
continue
}
fmt.Println("lock dir succeed, goroutine num=", num)
goto end
}
}
}
end:
time.Sleep(time.Second*2)
dirLock.Unlock() // 解锁
wg.Done()
}(i)
}
wg.Wait()
}