golang log日志里为什么需要加锁?

前言:

      有段时间没写博客了,这次写一篇水文,关于 golang log为什么加锁 ?  很水么? 对的…  

      有天同事跟我说,原来go log在写入的时候也有锁,我寻思不应该呀,为啥加锁呀。在我想来写个日志没必要加锁, 但事实来说不是这样的。 好多一些语言的日志模块也有加锁,比如 python logging 。 他加锁的目的在于 避免logrotate日志切割的时候,多线程发生冲突。  我们需要说明的是,单纯的写日志不需要加锁的,因为写日志采用了文件的O_APPEND模式,原子方式一直追加后面。 

该文章后续仍在不断的更新修改中, 请移步到原文地址  http://xiaorui.cc/?p=5195


原因

      那么golang 标准库里的log为啥加锁?  看下面的代码,我们在调用log.Print的时候,其实在调用Output。  output方法里有各种行为加锁,总结一句话,他写日志的对象是共享的,而不是每条日志分别一个Logger.buf 对象,既然不是分离的,那么要保护好buf了,要不然写串了。 

# xiaorui.cc

func (l *Logger) Output(calldepth int, s string) error {
	now := time.Now() // get this early.
	var file string
	var line int
	l.mu.Lock()
	defer l.mu.Unlock()
	if l.flag&(Lshortfile|Llongfile) != 0 {
		// Release lock while getting caller info - it's expensive.
		l.mu.Unlock()
		var ok bool
		_, file, line, ok = runtime.Caller(calldepth)
		if !ok {
			file = "???"
			line = 0
		}
		l.mu.Lock()
	}
	l.buf = l.buf[:0]
	l.formatHeader(&l.buf, now, file, line)
	l.buf = append(l.buf, s...)
	if len(s) == 0 || s[len(s)-1] != '\n' {
		l.buf = append(l.buf, '\n')
	}
	_, err := l.out.Write(l.buf)
	return err
}

func (l *Logger) Printf(format string, v ...interface{}) {
	l.Output(2, fmt.Sprintf(format, v...))
}

func (l *Logger) Print(v ...interface{}) { l.Output(2, fmt.Sprint(v...)) }

Logger 结构体对象

# xiaorui.cc

type Logger struct {
	mu     sync.Mutex // ensures atomic writes; protects the following fields
	prefix string     // prefix to write at beginning of each line
	flag   int        // properties
	out    io.Writer  // destination for output
	buf    []byte     // for accumulating text to write
}

日志加锁操作,听起来没啥问题,但如果你的日志很疯狂输出,那么问题就来了,这么多的syscall对于性能来说很是有杀伤力。  当然,正常线上系统肯定也没人去疯狂的benchmark日志,我这边只是为了测试说明问题,syscall 是有 代价的。

下面是阿里云四核主机的cpu占用比

pprof的火焰图分析也是runtime.futex高,剩下的就是syscall Write了。


总结

    没啥总结的。 只是好奇go log为啥频繁加锁而已,算是给我解除疑惑了吧。

END.


大家觉得文章对你有些作用! 如果想赏钱,可以用微信扫描下面的二维码,感谢!
另外再次标注博客原地址  xiaorui.cc