いろいろ備忘録日記

主に .NET とか Go とか Python絡みのメモを公開しています。

Goメモ-65 (ゴルーチンと再帰処理の組み合わせメモ) と C#のサンプル

概要

よくやり方忘れるので、自分用にここにメモ。

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

過去の記事については、以下のページからご参照下さい。

  • いろいろ備忘録日記まとめ

devlights.github.io

サンプルコードは、以下の場所で公開しています。

  • いろいろ備忘録日記サンプルソース置き場

github.com

github.com

github.com