zapはgoroutine safeなのか??
READMEに明記されてないので調べてた
(というかzapはドキュメント薄い。。。ソース読まないと分からん)
(基本goroutine safeと明記されてないものはgoroutine safeでは無いんだけど)
事の発端
zapのリポジトリのREADMEにはgoroutine-safeの記載は無かったので、zapの中でgoroutine-safeにするためのapiとかサポートされてないのかと思ってググったら、以下のissueを見つけた。
対応するPR
zapは実際にログを書き込む際に、io.Writerをwrapしたinterfaceであるzap.WriteSyncerというinterface使うのだけど、それを更にLock対応のLockWriteSyncerというstructにwrapする形になる。
以下は現在のmasterブランチのlogger.goからの抜粋
type lockedWriteSyncer struct { sync.Mutex ws WriteSyncer } // Lock wraps a WriteSyncer in a mutex to make it safe for concurrent use. In // particular, *os.Files must be locked before use. func Lock(ws WriteSyncer) WriteSyncer { if _, ok := ws.(*lockedWriteSyncer); ok { // no need to layer on another lock return ws } return &lockedWriteSyncer{ws: ws} } func (s *lockedWriteSyncer) Write(bs []byte) (int, error) { s.Lock() n, err := s.ws.Write(bs) s.Unlock() return n, err }
lockedWriteSyncerのWriteはWriteSyncerのwriteを叩く前にsync.Mutexでlockをかけるので、goroutine safeになっている。
lockedWriteSyncerもWriteSyncerのinterfaceを満たしているので、zapの他のAPIには影響がないというわけだ。
よって、WriteSyncerをzap.Lockに渡してwrapしてあげると、goroutine safeになる。
zapの標準プリセットはgoroutine safe?
zapには予め用意された設定(以後、便宜的に標準プリセットと呼ぶ)でロガーのインスタンスを作成する関数がいくつか用意されています。
zap.NewDevelopmentとか、zap.NewProductionとか
これらの関数で作成されるロガーは、goroutine safeなのかどうかソースを読んで確認した。
結論からいうと、goroutine safe
上述のzap.Lockの呼び出し箇所を確認したところ、Config構造体のopenSinksという関数の中で、引数に指定された複数のファイルパスからWriteSyncerを返すOpenという関数が叩かれており、その中で叩かれているCombineWriteSyncersという関数の中で、最後にzap.LockでWriteSyncerをwrapしたものをreturnしている。
なので、標準プリセットがどうというより、Config構造体のBuild関数を叩いて作成したLoggerについてはgoroutine safeとなるようだ。
ところでgodocをちゃんと読むと気になる記載が
以下、zapのlogger.goのLogger構造体の抜粋
// A Logger provides fast, leveled, structured logging. All methods are safe // for concurrent use. // // The Logger is designed for contexts in which every microsecond and every // allocation matters, so its API intentionally favors performance and type // safety over brevity. For most applications, the SugaredLogger strikes a // better balance between performance and ergonomics. type Logger struct { core zapcore.Core development bool name string errorOutput zapcore.WriteSyncer addCaller bool addStack zapcore.LevelEnabler callerSkip int }
「All methods are safe for concurrent use.」
はい??
InfoとかWarnとかのログ出力メソッドについては、zapcore.Coreの実装次第だと思うんだけども。。。?
このgodocの記載は信用していいのか。。。。?
そもそもio.Writer.writeがgoroutine safeだったりするケースありません?
zapでは(他のloggingライブラリもそうだと思うけど)最終的にio.Writerのwriteを叩くことになるので、io.Writerの実装次第では内部でlockをとっており、writeの呼び出し側ではlockしてあげる必要なかったりするのではと思って調べた。
(逆にいうと、io.Writerの実装依存なので、loggingライブラリ側ではwriteを叩く前にlockするのが妥当と言える)
とりあえず、os.Fileに関しては、writeの内部でlockがかかってるように見える
以下はfd_unix.goからの抜粋
// Write implements io.Writer. func (fd *FD) Write(p []byte) (int, error) { if err := fd.writeLock(); err != nil { return 0, err } defer fd.writeUnlock() if err := fd.pd.prepareWrite(fd.isFile); err != nil { return 0, err } var nn int for { max := len(p) if fd.IsStream && max-nn > maxRW { max = nn + maxRW } n, err := syscall.Write(fd.Sysfd, p[nn:max]) if n > 0 { nn += n } if nn == len(p) { return nn, err } if err == syscall.EAGAIN && fd.pd.pollable() { if err = fd.pd.waitWrite(fd.isFile); err == nil { continue } } if err != nil { return nn, err } if n == 0 { return nn, io.ErrUnexpectedEOF } } }
先頭行の方で何やらlockをかけているのが分かる。
しかし、この実装はgo1.9からなんだよね。
go1.8ではfd_unix.goのパッケージ階層自体違うし、上記のようなlockはかかっていない。
そして、go1.9のリリースノートを確認したところ、以下の記載がある。
The os package now uses the internal runtime poller for file I/O. This reduces the number of threads required for read/write operations on pipes, and it eliminates races when one goroutine closes a file while another is using the file for I/O.
goroutine safeにしたとは言ってないような。。。???
(結果的にgoroutine safeになってるかもしれないけど)
これは非goroutine safeとして扱ったほうが良さそう