问题
写了一段用来检索文件夹的代码,使用协程加快检索速度
过程中出现了两个问题:
- 文件数量不正确,会缺失,少则几个,多则几千
- 程序报错:WaitGroup is reused before previous Wait has returned
代码呈上
逻辑代码 file.go
:
package file
import (
"fmt"
"gopatch/cmd"
"io"
"io/fs"
"log"
"os"
"path/filepath"
"strings"
"sync"
"sync/atomic"
"time"
)
// FindAllFilesGo 对外的协程遍历文件,封装同步工具
func FindAllFilesGo(allFileMap *sync.Map, path string) {
// 文件对应锁 Map
var fileLockMap sync.Map
var wg sync.WaitGroup
_, err := os.Stat(path)
if err != nil {
panic(fmt.Sprintf(`遍历文件路径不正确 %s : %v`, path, err.Error()))
}
log.Printf("开始启用协程遍历 %s 所有文件...", path)
findAllFilesGo(allFileMap, &fileLockMap, path, &wg)
wg.Wait()
log.Printf("所有文件遍历结束....")
// 睡一秒在很大程度上保证遍历的文件数量准确
time.Sleep(1 * time.Second)
}
// findAllFilesGo 使用协程加快遍历文件速度
func findAllFilesGo(allFileMap *sync.Map, fileLockMap *sync.Map, path string, wg *sync.WaitGroup) {
wg.Add(1)
defer wg.Done()
dir, _ := os.ReadDir(path)
for _, entry := range dir {
if entry.IsDir() {
go findAllFilesGo(allFileMap, fileLockMap, filepath.Join(path, entry.Name()), wg)
} else {
// 是文件
// 处理逻辑
}
}
}
测试代码 file_test.go
:
package file
import (
"fmt"
"sync"
"testing"
)
func TestFindAllFilesGo(t *testing.T) {
failCount := 0
for i := range 10000 {
var allFilesMap sync.Map
// 建议测试用文件夹下深度足够大,文件数量足够多
FindAllFilesGo(&allFilesMap, "D:\\xxxxxxxxxxxx")
var counter = 0
allFilesMap.Range(func(_, val any) bool {
list := val.(*[]string)
counter += len(*list)
return true
})
fmt.Printf("第 %d 次扫描文件数:%d\n", i, counter)
if counter != 42244 {
t.Errorf("文件数不对")
failCount++
}
}
fmt.Printf("失败次数:%d\n", failCount)
if failCount > 0 {
t.Fail()
}
}
如同入口方法 FindAllFilesGo
中,我使用了 time.Sleep
让协程最后睡一秒,这在很大程度上减少了文件检索数量不正确的问题(但没有根除),而 panic 仍然偶尔出现
一开始,我以为 panic 的原因在于 wg.Wait 的重复调用,但是实际上使用以下代码测试发现,并不会出现这个 panic
import (
"fmt"
"sync"
"time"
)
func main() {
wg := sync.WaitGroup{}
wg.Add(1)
go func() {
time.Sleep(1 * time.Second)
fmt.Println("rewait")
wg.Wait()
}()
go func() {
time.Sleep(2 * time.Second)
wg.Done()
}()
wg.Wait()
}
可知,wg.Wait
内部的 waiter 变为 0 后,Wait 方法已经开始进行后续步骤,然而外面又调用了 wg.Add
方法,导致违反了 “如果重用WaitGroup来等待几个独立的事件集,则必须在所有先前的wait调用返回之后发生新的Add调用”
最终就出现了 panic: WaitGroup is reused before previous Wait has returned
而这也同时是文件数量不正确的原因:在 Wait 方法执行完毕后,后续的递归没有被阻塞,整个程序就结束了
解决
我的解决方法很简单,在 wg.Done
之前先睡一会儿,保证前面的协程开始执行到 wg.Add
即可
defer func() {
// 在 Done 之前稍等一会
/**
因为递归过程中可能出现下面的 go 协程已注册但未执行到 wg.Add ,然而外面的已经执行完并 wg.Done
导致最外面的 wg.Wait 被放行,此时出现两种错误;
1. 文件遍历数不正确
2. WaitGroup is reused before previous Wait has returned
*/
time.Sleep(10 * time.Millisecond)
wg.Done()
}()
总结
任何语言的多线程都相似,最麻烦的地方在于无法用一般性的思维考虑执行步骤,很容易遗漏一些地方的执行顺序
这就要有足够多的经验支撑了