概要
よくやり方忘れるので、自分用にここにメモ。
WaitGroupの待ち合わせとチャネルのcloseを忘れるとGoにdeadlockって怒られるので注意。
サンプル
ディレクトリを再帰的に降りていって出力するサンプル。別に非同期である必要はないのですがサンプルってことで。
以下の点を実現できるようにしています。
- 再帰処理が非同期で実行されている
- 非同期実行しているけど、結果の出力は順序を守る
- ioutils.ReadDir() の結果の順序を崩さない
- メイン処理は全部の非同期処理が終わるまできっちり待つ
package async import ( "fmt" "io/ioutil" "path/filepath" "strings" "sync" ) // DirWalkRecursive は、非同期処理と再帰処理の組み合わせのサンプルです。 func DirWalkRecursive() error { var ( wg = sync.WaitGroup{} // 待ち合わせ用 ch = make(chan string) // メイン処理と非同期処理との間でデータを受け渡すためのチャネル ) // -------------------------------------------------- // 再帰しながらディレクトリツリーを下り、情報を出力 // -------------------------------------------------- wg.Add(1) dir, _ := filepath.Abs(".") go listdir(dir, &wg, ch, 1) // -------------------------------------------------- // 終わる時間は不定なため、再帰処理にデータを処理させながら // 同時に出力を実施。再帰処理完了とともに出力処理を止める。 // -------------------------------------------------- go func() { wg.Wait() close(ch) }() for v := range ch { fmt.Println(v) } return nil } func listdir(dir string, wg *sync.WaitGroup, ch chan<- string, depth int) { defer wg.Done() var ( chSubDirs = make([]chan string, 0, 0) dprefix = strings.Repeat("\t", depth-1) // ディレクトリ用のプレフィックス fprefix = strings.Repeat("\t", depth) // ファイル用のプレフィックス ) // ディレクトリ名を出力 d := dir if depth > 1 { d = filepath.Base(dir) } ch <- fmt.Sprintf("%s%s", dprefix, d) // -------------------------------------------------- // 自身の配下を非同期で処理 // -------------------------------------------------- files, _ := ioutil.ReadDir(dir) for _, f := range files { if f.IsDir() { // ドットで始まるディレクトリは無視 if strings.HasPrefix(f.Name(), ".") { continue } // 配下に対して非同期で探索開始 chSubDir := make(chan string) chSubDirs = append(chSubDirs, chSubDir) wgSubDir := sync.WaitGroup{} wgSubDir.Add(1) go listdir(filepath.Join(dir, f.Name()), &wgSubDir, chSubDir, depth+1) // 再帰処理を非同期で待ち合わせ go func() { wgSubDir.Wait() close(chSubDir) }() } else { // ファイルの場合は出力 ch <- fmt.Sprintf("%s%s", fprefix, f.Name()) } } // 配下の非同期処理を実施しながら、結果を親のチャネルに入れていく for _, subCh := range chSubDirs { for v := range subCh { ch <- v } } }
try-golang/dir_walk_recursive.go at master · devlights/try-golang · GitHub
実行すると以下のようになります。
$ make run ENTER EXAMPLE NAME: async_dir_walk_recursive [Name] "async_dir_walk_recursive" github.com\devlights\try-golang .gitignore .gitpod.Dockerfile .gitpod.yml Dockerfile LICENSE Makefile README.md go.mod go.sum books doc.go examples.go bootcamp GoBootcamp.pdf doc.go concurrency doc.go go101 Go101-v1.13.m.pdf doc.go startingGo doc.go builder builder.go doc.go cmd trygolang args.go errs.go exec.go ifs.go main.go runloop.go runonce.go utils.go effectivego doc.go effectivego_01_introduction.go ・ ・ ・
ついでに C#
のサンプル
できるだけ同じような感じで書いてみた。勿体ないので、ここにメモ。C#
は、TaskとBlockingCollectionがあるからGoと同じくらい楽ですねーやっぱり。
using System.Collections.Concurrent; using System.Collections.Generic; using System.IO; using System.Threading.Tasks; using TryCSharp.Common; namespace TryCSharp.Samples.Async { public class DirWalkRecursiveAsync : IAsyncExecutable { public async Task Execute() { var depth = 1; var dir = Path.GetFullPath(@"."); var buf = new BlockingCollection<string>(); // --------------------------------------------------------- // 再帰的にディレクトリを降りていき、ファイルを出力 // --------------------------------------------------------- var listDirTask = Task.Run<Task>(async () => { await this.ListDir(dir, buf, depth); buf.CompleteAdding(); }); // --------------------------------------------------------- // 結果を順次出力 // --------------------------------------------------------- var outputTask = Task.Run(() => { foreach (var item in buf.GetConsumingEnumerable()) { Output.WriteLine(item); } }); await Task.WhenAll(listDirTask, outputTask); } private async Task ListDir(string dir, BlockingCollection<string> buf, int depth) { var subDirBufs = new List<BlockingCollection<string>>(); var dirPrefix = new string('\t', depth - 1); var filePrefix = new string('\t', depth); var d = dir; if (depth > 1) { d = Path.GetFileName(dir); } buf.Add($"{dirPrefix}{d}"); // --------------------------------------------------------- // サブディレクトリに対して、非同期で再帰処理実行 // --------------------------------------------------------- var tasks = new List<Task>(); foreach (var subD in Directory.EnumerateDirectories(dir, "*", SearchOption.TopDirectoryOnly)) { if (Path.GetFileName(subD).StartsWith(".")) { continue; } var bufSubDir = new BlockingCollection<string>(); subDirBufs.Add(bufSubDir); var subDir = Path.Combine(dir, subD); tasks.Add(Task.Run(async () => { await this.ListDir(subDir, bufSubDir, depth + 1); bufSubDir.CompleteAdding(); })); } // --------------------------------------------------------- // ファイルはそのまま出力 // --------------------------------------------------------- foreach (var subF in Directory.EnumerateFiles(dir, "*.*", SearchOption.TopDirectoryOnly)) { buf.Add($"{filePrefix}{Path.GetFileName(subF)}"); } // --------------------------------------------------------- // 非同期処理の結果を親のバッファに順次投入 // --------------------------------------------------------- await Task.WhenAll(tasks).ContinueWith(task => { foreach (var subDirBuf in subDirBufs) { foreach (var item in subDirBuf.GetConsumingEnumerable()) { buf.Add(item); } } }); } } }
try-csharp/DirWalkRecursiveAsync.cs at master · devlights/try-csharp · GitHub
実行すると以下のようになります。
$ dotnet run --project TryCSharp.Tools.Cui\TryCSharp.Tools.Cui.csproj ENTER CLASS NAME: DirWalk ================== START ================== [Async] **** BEGIN **** try-csharp .gitignore .gitpod.Dockerfile .gitpod.yml LICENSE Makefile README.md try-csharp.sln try-csharp.sln.DotSettings.user TryCSharp.Common IAsyncExecutable.cs IExecutable.cs IExecutor.cs IHasDefence.cs IHasValidation.cs IInputManager.cs Input.cs IOutputManager.cs Output.cs SampleAttribute.cs TryCSharp.Common.csproj bin Debug netcoreapp2.1 TryCSharp.Common.deps.json TryCSharp.Common.dll TryCSharp.Common.pdb netcoreapp2.2 TryCSharp.Common.deps.json TryCSharp.Common.dll TryCSharp.Common.pdb netcoreapp3.1 TryCSharp.Common.deps.json TryCSharp.Common.dll TryCSharp.Common.pdb ・ ・ ・ [Async] **** END **** ================== END ================== Elapsed Time: 00:00:00.1522018
過去の記事については、以下のページからご参照下さい。
- いろいろ備忘録日記まとめ
サンプルコードは、以下の場所で公開しています。
- いろいろ備忘録日記サンプルソース置き場