背景

在一个无聊的下午,我突发奇想统计任意目录下的文件个数。我也是进入程序界快两年的人勒,于是当我想从单线程进军多线程领域。未曾想到,死锁成为了我不可磨灭的噩梦。

基本功能需求

  1. 不漏数;
  2. 限制并发量,避免cpu频繁发生上下文切换

一个工人辛勤劳作版

func countFile(path string) {
	if infos, err := ioutil.ReadDir(path); err != nil {
		fmt.Println("some errors curd")
		return
	} else {
		for _, file := range infos {
			if file.IsDir() {
				findFile(path + "/" + file.Name() + "/")
				continue
			}
			countFiles++
		}
	}
}

只要目录的深度没有超过最大程序栈的容量,这个工人是可以正常工作,当然这里就不考虑那么深目录了。我们写个主函数编译运行一下:

var var s *string
var countFiles int32 = 0
func init() {
	s = flag.String("f", ".", "use for path") // 默认当前执行目录
}
func main() {
	flag.Parse()
	start := time.Now()
	countFile(*s)
	fmt.Println("findFiles", time.Now().Sub(start).Milliseconds())
	fmt.Println(countFiles,0)
}
 go build ../test
 ./test -f /usr

运行结果:
在这里插入图片描述
一个工人要数24万个文件,心里肯定不平衡,为了公平起见我们让其他工人也参加这个数文件工作。

进入正片,雨露均沾版

思路:
每当遇到一个目录我们就让一个工人去数目录下的文件,当然工人不能无限分配,所以我们需要限制工人的数量。其次我们需要一个监工来负责安排工人,和统计结果,掌握整个进度。

package main

import (
	"flag"
	"fmt"
	"io/ioutil"
	"strings"
	"sync/atomic"
	"time"
)

var s *string

var (
	countFiles  = 0
	res         = make(chan bool)
	path        = make(chan string)
	workDone    = make(chan bool)
	works       = make(chan struct{},10)
)

func init() {
	s = flag.String("f", ".", "use for path")
}
func main() {
	flag.Parse()
	start := time.Now()
	works <- struct{}{}
	go multiFindFile(*s, true)
	deal()
	first := time.Now()
	fmt.Println("multiFind", first.Sub(start).Milliseconds())
}
// 监工
func deal() {
	for true {
		select {
		case <-res:
			countFiles++
		case path_ := <-path:
			works <- struct{}{}
			go multiFindFile(path_, true)

		case <-workDone:
			 <- works 
			if len(works) == 0 {
				return
			}
		}
	}
}
// 工人
func multiFindFile(path_ string) {
	if infos, err := ioutil.ReadDir(path_); err == nil {
		for _, file := range infos {
			if file.IsDir() {
				path <- path_ + "/" + file.Name() + "/"
				continue
			}
			res <- true
		}
		workDone <- true
	}
	
}

运行发现出现可怕的deadlock字样。那这个思路哪里会发生死锁问题呢?
根据描述的思路来看,我们采用了监工来安排工人,工人只愿意负责自己目录的文件而不愿意负责目录,而工人总数肯定是小于目录数,就会出现工人在等监工安排工人执行子目录,而监工在等工人完成当前目录。就会出现你等我,我等你的尴尬情况。专业称之为死锁。因而需要避免这一情况,则需要增加工人,或则给工人增加工作量。这里就不运行展示了,运行必然出现deadlock。下面看看“公正的”解决方案

解决方案

// 工人做的工作,本方案只能强行让工人做(加钱做不做?)。我(打工人):做做做(^.^)
func multiFindFile(path_ string, isMaster bool) {
	/*
		isMaster: true代表是创建了新的工人来做子目录工作;false代表工人用完了,只能工人自己负责旗下的所有目录文件
	*/
	if infos, err := ioutil.ReadDir(path_); err == nil {
		for _, file := range infos {
			if file.IsDir() {
				if atomic.AddInt32(&works, -1) >= 0 {
					path <- path_ + "/" + file.Name() + "/"
					continue
				}
				multiFindFile(path_+"/"+file.Name()+"/", false)
				continue
			}
			res <- true
		}
		if isMaster {
			workDone <- true
		}
	}
}
// 监工做的工作
func deal() {
	for true {
		select {
		case <-res:
			countFiles++
		case path_ := <-path:
			go multiFindFile(path_, true)

		case <-workDone:
			if atomic.AddInt32(&works, 1) == 64 {
				return
			}
		}
	}
}

这样就可以正常运转,这个就交给读者朋友去测试了,其中还存在一些bug(例如还是会诞生一些不存在的工人来完成了工作,监工都觉得很诡异,无所谓反正我没花钱),读者可以自己修复一下,别让监工赚了黑心钱。
|
|
|
|
今天就到这里啦,该休息一下了(偷偷告诉你们,我测试过很多遍才想明白,没办法我太菜)。

思路参考:https://www.bilibili.com/video/BV1qT4y1c77u?spm_id_from=333.1007.top_right_bar_window_history.content.click
详见代码。