Go语言Mutex未正确锁定资源:文件切换后Goroutine读取异常
问题原因与解决方案
问题原因
核心问题出在共享的*os.File是有状态对象:
os.File内部维护着文件读取的偏移量,当第一个goroutine调用io.ReadAll读完整个文件后,偏移量会移动到文件末尾。后续goroutine再对同一个*os.File调用ReadAll时,会从末尾开始读取,自然返回空内容。- 切换文件的逻辑虽然用了互斥锁保证同步,但替换的只是
Root.File指针;在两次文件切换的间隔内,多个goroutine共享同一个*os.File,偏移量被第一个读操作耗尽,导致后续读空。
解决方案
方案1:读取前重置文件偏移量
在每个goroutine读取文件前,先将文件偏移量重置到开头,且该操作要在读锁保护下进行(Seek会修改文件状态,需要同步):
go func(i int, root *Root) { root.RLock() defer root.RUnlock() // 重置文件偏移量到开头 _, err := root.File.Seek(0, io.SeekStart) if err != nil { fmt.Printf("go routine %v seek error: %v\n", i, err) return } content, err := io.ReadAll(root.File) if err != nil { fmt.Printf("go routine %v read error: %v\n", i, err) return } fmt.Printf("go routine %v is reading data %s\n", i, content) }(i, root)
该方案无需修改文件切换逻辑,仅通过重置偏移量保证每次读取从文件开头开始。
方案2:维护文件名而非文件指针
更稳妥的方式是不在Root中保存*os.File,而是维护当前要读取的文件名。每次读取时独立打开文件、读完关闭,避免共享有状态的文件句柄:
- 修改
Root结构体:
type Root struct { Filename string sync.RWMutex }
- 修改
startTask的读取逻辑:
go func(i int, root *Root) { root.RLock() filename := root.Filename root.RUnlock() // 每次读取独立打开文件 file, err := os.Open(filename) if err != nil { fmt.Printf("go routine %v open error: %v\n", i, err) return } defer file.Close() content, err := io.ReadAll(file) if err != nil { fmt.Printf("go routine %v read error: %v\n", i, err) return } fmt.Printf("go routine %v is reading data %s\n", i, content) }(i, root)
- 修改
startWorker的文件切换逻辑:
func startWorker() { interval := 4 * time.Second ticker := time.NewTicker(interval) defer ticker.Stop() f1 := "concurrent/1/f1.txt" f2 := "concurrent/2/f2.txt" f := f1 root := &Root{Filename: f} go startTask(root) for { root.Lock() if f == f2 { f = f1 } else { f = f2 } root.Filename = f root.Unlock() <-ticker.C } }
该方案彻底避免了共享文件句柄带来的状态干扰,每个goroutine独立操作文件,安全性更高。
内容的提问来源于stack exchange,提问作者Contour Free
相关产品推荐
相关产品推荐

