goroutine で os.Chdir() する際に注意すべきポイント

この記事は [Go Advent Calendar 2021](https://qiita.com/advent-calendar/2021/go) 18 日目の記事です。 

題のような記事を日本語のウェブサイト/ブログで紹介されているのを目にしないため、題のテーマを選びました。 

## 環境のバージョン

* Windows10
* go version go1.17.5 windows/amd64

## goroutine で os.Chdir() を行うとこのようなことがおこる

まずは、実際に `goroutine` で `os.Chdir()` を実施するとどのような動きになるかを見てみましょう。 
以下のようなサンプルプログラムを動かしてみます。 

```main.go
package main

import (
	"fmt"
	"os"
	"path/filepath"
	"sync"
	"time"
)

var wg sync.WaitGroup

func main() {
	baseDir, _ := os.Getwd()
	wg.Add(3)
	for i := 1; i < 4; i++ {
		go doSomethingWithChdir(baseDir, i)
	}
	wg.Wait()
}

func doSomethingWithChdir(baseDir string, num int) {
	defer wg.Done()
	wd, _ := os.Getwd()
	fmt.Printf("routine %v: starting in dir: %s\n", num, wd)
	os.Chdir(filepath.Join(baseDir, fmt.Sprintf("%v", num)))
	wd, _ = os.Getwd()
	fmt.Printf("routine %v: going to sleep in dir: %s\n", num, wd)
	time.Sleep(1000 * time.Millisecond)
	wd, _ = os.Getwd()
	fmt.Printf("routine %v: woke up in dir: %s\n", num, wd)
}
```
上記のコードは [dpastoor/changeDirInGoroutine.go](https://gist.github.com/dpastoor/fc349f9874991e7417b072e7dcbc94ad) を参考に一部を変更して作成しました。

実行すると、以下のような出力となります。 
いくつかの goroutine が、コードで (少なくとも筆者の) 意図しないディレクトリとなってしまいます。 
なんとなく、最後に実行された goroutine が設定したディレクトリが全ての goroutine へ反映されているように見て取れます。 

```
$ xxx.exe
routine 3: starting in dir: C:\Users\aki01\work\src\github.com\akif999\goroutine_chdir_example
routine 1: starting in dir: C:\Users\aki01\work\src\github.com\akif999\goroutine_chdir_example
routine 2: starting in dir: C:\Users\aki01\work\src\github.com\akif999\goroutine_chdir_example
routine 3: going to sleep in dir: C:\Users\aki01\work\src\github.com\akif999\goroutine_chdir_example\3
routine 1: going to sleep in dir: C:\Users\aki01\work\src\github.com\akif999\goroutine_chdir_example\1
routine 2: going to sleep in dir: C:\Users\aki01\work\src\github.com\akif999\goroutine_chdir_example\2
routine 2: woke up in dir: C:\Users\aki01\work\src\github.com\akif999\goroutine_chdir_example\2
routine 1: woke up in dir: C:\Users\aki01\work\src\github.com\akif999\goroutine_chdir_example\2 # goroutine 内で設定したものとは異なるディレクトリになっている
routine 3: woke up in dir: C:\Users\aki01\work\src\github.com\akif999\goroutine_chdir_example\2 # 同上
```

なお、同様の疑問はインターネットで検索をすると、海外のサイトでいくつかヒットします。 
本記事と併せて参考にされてください。 

* [I have an issue with goroutines](https://forum.golangbridge.org/t/i-have-an-issue-with-goroutines/23077)
* [Changing directories in go routines](https://stackoverflow.com/questions/33001411/changing-directories-in-go-routines)

## なぜこのようなことが起こるのか

それでは、どうしてこのようなことが起こるのかを考えてみたいと思います。 

まず、私は前章へ記載した通り、「ある goroutine での os.Chdir() が、他の goroutine へ影響を及ぼしている」、 
つまり、「ディレクトリという属性はプロセスにつき一つであるため、プロセスを隔てていない goroutine ごとに異なるディレクトリは設定できない」と考えました。 

上記の仮説について、goroutine の仕組みや、プロセスとカレントディレクトリの関係などについて調べたところ、おおよそ上記の説明で正しいことがわかりました。 
しかし、その内容について本記事では簡単にまとめることができませんでした。 

よって、Go 公式の Issue を調べて、上記の動作について議論されたものがあったため、そちらの内容を抜粋して紹介します。

[os: Chdir is not reentrant](https://github.com/golang/go/issues/27658)

上記の Issue にて以下のようなやりとりが実施されていました。 
(以下、本記事で理解し易いように意訳しています)

* os.Chdir は再入可能となっておらず、goroutine 間で状態が共有されておりバグである (質問者)
* os.Chdir はプロセスの属性であり、 goroutine やスレッドごとの属性ではない よって質問の動作となることは意図通りである (回答者)
    * また、(修正方法はないが) 仮に修正したとしても、現状の動作を期待するソフトが壊れてしまうため変更することができない (回答者)
* Python や Java では当該問題については対処できるように実装されている (質問者)
* Java での対処は Go における os.Chdir に相当することを実施しておらず、 os/exec.Cmd に相当することを実施しており、そちらは Go でも利用できる (回答者)

## Go ではどのようにディレクトリごとの処理を実施すればよいか

goroutine ごとに異なるディレクトリを取り扱う場合は、 os/exec.Cmd を使うようにすれば概ね解決すると思われます。 
なぜなら、 os/exec.Cmd の実行時は別プロセスを起動して実施するためです。  
以下のように、 `cmd.Dir` へ任意のディレクトリを設定することで、起動するプロセスのカレントディレクトリを変更することができます。 

```go
cmd := exec.Command("git", "log")
cmd.Dir = "path/to/working/directory"
out, err := cmd.Output()
```

## まとめ

* goroutine で `os.Chdir` を実行する際は goroutine 間で状態が共有されることに注意する
* goroutine はプロセスを隔てておらず、同一プロセスの中で作成されることに留意する
* goroutine ごとに異なるディレクトリで処理を実施したい場合は、 os/exec.Cmd を使用して別プロセスを起動するようにする

## 参考文献

* [dpastoor/changeDirInGoroutine.go](https://gist.github.com/dpastoor/fc349f9874991e7417b072e7dcbc94ad)
* [I have an issue with goroutines](https://forum.golangbridge.org/t/i-have-an-issue-with-goroutines/23077)
* [Changing directories in go routines](https://stackoverflow.com/questions/33001411/changing-directories-in-go-routines)
* [os: Chdir is not reentrant](https://github.com/golang/go/issues/27658)