【Golang】对大文件夹使用协程递归时出现的错误

1 篇文章 0 订阅
1 篇文章 0 订阅

问题

写了一段用来检索文件夹的代码,使用协程加快检索速度
过程中出现了两个问题:

  1. 文件数量不正确,会缺失,少则几个,多则几千
  2. 程序报错: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()
	}()

总结

任何语言的多线程都相似,最麻烦的地方在于无法用一般性的思维考虑执行步骤,很容易遗漏一些地方的执行顺序

这就要有足够多的经验支撑了

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值