标签 goroutine 下的文章

近期遇到的3个Golang代码问题

这两周来业余时间都在用Golang写代码,现在处于这样一个状态:除了脚本,就是Golang了。反正能用golang实现的,都用golang写。

Golang语言相对成熟了,但真正写起来,还是要注意一些“坑”的,下面是这周遇到的三个问题,这里分享出来,希望能对遇到同样问题的童鞋有所帮助。

一、误用定时器,狂占CPU

golang中有一个通过channel实现timeout或tick timer的非常idiomatic的方法,代码如下:

func worker(start chan bool) {
        for {
                timeout := time.After(30 * time.Second)
                 select {
                         // … do some stuff
                         case <- timeout:
                                 return
                 }
        }
}

func worker(start chan bool) {
        for {
                heartbeat := time.Tick(30 * time.Second)
                 select {
                         // … do some stuff
                         case <- heartbeat:
                                 return
                 }
        }
}

没错,就像上面这两个例子,如果你单独执行它们,你不会发现任何问题,但是当你将这样的代码放到一个7 * 24小时的Service中,并且timeout间隔或heartbeat间隔为更短时间,比如1s时,问题就出现了。

我的程序最初就是用上面的代码实现了一个timewheel,通过放置在一个单独goroutine中的定时器检测timewheel是否有到期的 timer。程序跑在后台运行的很好,直到有一天晚上我无意中执行了一下top,我发现这个service居然站用了40%多的CPU负荷。最初我怀疑是 不是代码中有死循环,但仔细巡查一遍代码后,没有发现死循环的痕迹,算法逻辑也没问题。

于是重启了一下这个service,发现cpu占用降了下来。出去去了趟卫生间,回来继续用top观察,不好,这个service占用了1%的CPU,再 过一会升到2%,观察一段时间后,发现这个service对cpu的占用率随着时间的推移而增加。gdb attach了相应的进程号,stack多是go runtime的调度。

再次回到代码,发现可能存在问题的只有这里的tick。我的tick间隔是1s。这样每1s都会创建一个runtime timer,而通过runtime的源码来看,这些timer都扔给了runtime调度(一个heap)。时间长了,就会有超多的timer需要 runtime调度,不耗CPU才怪。

于是做了如下修改:

func worker(start chan bool) {
        heartbeat := time.Tick(1 * time.Second)
         for {
               
                 select {
                         // … do some stuff
                         case <- heartbeat:
                                 return
                 }
        }
}

重新编译执行service,观察了一天,cpu再也没有升高过。

二、小心list.List的Delete逻辑

其实这是一个在哪种语言中都会遇到的初级问题,这里只是给大家提个醒罢了。不多说了,上代码:

从一个list.List中删除一个element,一般逻辑是:

l := list.New()
… …
for e := l.Front(); e != nil; e = e.Next() {
        if e.Value.(int) == someValue {
                l.Remove(e)
                return or break
        }
}

但是如果list里有重复元素,且代码要遍历整个list删除某个值为somevalue的元素呢?上面的一般方法是由逻辑缺陷的,例子:

func foo(i int) {
        l := list.New()
        for i := 0; i < 9; i++ {
                l.PushBack(i)
        }
        l.PushBack(6)

        for e := l.Front(); e != nil; e = e.Next() {
                fmt.Print(e.Value.(int))
        }

        for e := l.Front(); e != nil; e = e.Next() {
                if e.Value.(int) == i {
                        l.Remove(e)
                }
        }

        fmt.Printf("\n")
        for e := l.Front(); e != nil; e = e.Next() {
                fmt.Print(e.Value.(int))
        }
        fmt.Printf("\n")
}

func main() {
        foo(6)
}

该程序试图删除list中的所有值为6的element,但执行结果却是:

go run testlist.go
0123456786
012345786

list中尾部的那个6没有被删除,程序似乎在删除完第一个6之后就不再继续循环了。事实也是这样:

当l.Remove(e)执行后,e.Next()被置为了nil,这样循环条件不再满足,循环终止。

为此,对于这样的程序,下面的方法才是正确的:

func main() {
        bar(6)
}

func bar(i int) {
        l := list.New()
        for i := 0; i < 9; i++ {
                l.PushBack(i)
        }
        l.PushBack(6)

        for e := l.Front(); e != nil; e = e.Next() {
                fmt.Print(e.Value.(int))
        }

        var next *list.Element
        for e := l.Front(); e != nil; {
                if e.Value.(int) == i {
                        next = e.Next()
                        l.Remove(e)
                        e = next
                } else {
                        e = e.Next()
                }
        }

        fmt.Printf("\n")
        for e := l.Front(); e != nil; e = e.Next() {
                fmt.Print(e.Value.(int))
        }
        fmt.Printf("\n")
}

执行结果:
$ go run testlist.go
0123456786
01234578

三、要给template起个正确的名字

编写一个Web程序,需要用到html/template。

… …
t := template.New("My Reporter")
t, err = t.ParseFiles("views/report.html")
if err != nil {
        w.WriteHeader(http.StatusInternalServerError)
        return
}

t.Execute(w, UserInfo{xx: XX})

结果一执行却crash了:

[martini] PANIC: runtime error: invalid memory address or nil pointer dereference
/usr/local/go/src/runtime/panic.go:387 (0×16418)
/usr/local/go/src/runtime/panic.go:42 (0x1573e)
/usr/local/go/src/runtime/sigpanic_unix.go:26 (0x1bb50)
/usr/local/go/src/html/template/template.go:59 (0x7ed64)
/usr/local/go/src/html/template/template.go:75 (0x7ef0d)
/Users/tony/Test/GoToolsProjects/src/git.oschina.net/bigwhite/web/app.go:104 (0x2db0)
    reportHandler: t.Execute(w, UserInfo{xx: XXX})

问题在t.Execute这行,单独把template代码摘出来放在一个测试代码中:

//testtmpl.go
type UserInfo struct {
        Name string
}

func main() {
        t := template.New("My Reporter")
        t, err := t.ParseFiles("views/report.html")
        if err != nil {
                fmt.Println("parse error")
                return
        }

        err = t.Execute(os.Stdout, UserInfo{Name: "tonybai"})
        if err != nil {
                fmt.Println("exec error", err)
        }
        return
}

执行结果:
go run testtmpl.go
exec error template: My Reporter: "My Reporter" is an incomplete or empty template; defined templates are: "report.html"

看起来似乎template对象与模板名字对不上导致的错误啊。修改一下:

t := template.New("report.html")

执行结果:

<html>
<head>
</head>
<body>
    Hello, tonybai
</body>
</html>

这回对了,看来template的名字在与ParseFiles一起使用时不是随意取的,务必要与模板文件名字相同。

ParseFiles支持解析多个文件,如果是传入多个文件该咋办?godoc说了,template名字与第一个文件名相同即可。

Goroutine是如何工作的

golangweekly的第36期Go Newsletter中我发现一篇短文"How Goroutines Work" ,其作者在参考了诸多资料后,简短概要地总结了一下 Goroutine的工作原理,感觉十分适合刚入门的Gophers(深入理解Goroutine调度的话,可以参考Daniel Morsing的" The Go scheduler" )。这里粗译如下。

一、Go语言简介

如果你是Go语言新手,或如果你对"并发(Concurrency)不是并行(parallelism)"这句话毫无赶脚,那么请看一下Rob Pike大神关于这个主题的演讲吧,演讲共30分 钟,我敢保证你在这个演讲上花费30分钟是绝对值得的。

总结一下两者(Concurrency和Parallelism)的不同:"当人们听到并发(Concurrency)这个词时,总是会想起并行 (Parallelism),它们之间有相关性,但却是两个明显不同的概念。在编程领域,并发(Concurrency)是独立的执行过程 (Process)的组合,而并行(Parallelism)则是计算(可能是相关联的)的同时执行。并发(Concurrency)是关于同时 应对很多事情(deal with lots of things),而并行(Parallelism)则是同时做许多事情(do lots of things)"。(Rob Pike的“Concurrency is not parallelism")

Go语言支持我们编写并发(Concurrent)的程序。它提供了Goroutine以及更重要的在Goroutines之间通信的能力。这里 我们将聚焦在前者(译注:指并发)。

二、Goroutines和Threads

Goroutine是一个简单的模型:它是一个函数,与其他Goroutines并发执行且共享相同地址空间。Goroutines的通常用法是根据需要创建尽可 能的Groutines,成百上千甚至上万的。这种用法对于那些习惯了使用C++或Java的程序员来讲可能会有些奇怪。创建这么多 goroutines势必要付出不菲的代价?一个操作系统线程使用固定大小的内存作为它的执行栈,当线程数增多时,线程间切换的代价也是相当的 高。这也是每处理一个request就创建一个新线程的服务程序方案被诟病的原因。

不过Goroutine完全不同。它们由Go运行时初始化并调度,操作系统根本看不到Goroutine的存在。所有的goroutines都是 活着的,并且以多路复用的形式运行于操作系统为应用程序分配的少数几个线程上。创建一个Goroutine并不需要太多内存,只需要8K的栈空间 (在Go 1.3中这个Size发生了变化)。它们根据需要在堆上分配和释放内存以实现自身的增长。

Go运行时负责调度Goroutines。Goroutines的调度是协作式的,而线程不是。这意味着每次一个线程发生切换,你都需要保存/恢 复所有寄存器,包括16个通用寄存器、PC(程序计数器)、SP(栈指针)、段寄存器(segment register)、16个XMM寄存器、FP协处理器状态、X AVX寄存器以及所有MSR等。而当另一个Goroutine被调度时,只需要保存/恢复三个寄存器,分别是PC、SP和DX。Go调度器和任何现代操作 系统的调度器都是O(1)复杂度的,这意味着增加线程/goroutines的数量不会增加切换时间,但改变寄存器的代价是不可忽视的。

由于Goroutines的调度是协作式的,一个持续循环的goroutine会导致运行于同一线程上的其他goroutines“饿死”。在 Go 1.2中,这个问题或多或少可以通过在进入函数前间或地调用Go调度器来缓解一些,因此一个包含非内联函数调用的循环是可以被调度器抢占的。

三、Goroutine阻塞

只要阻塞存在,它在OS线程中就是不受欢迎的,因为你拥有的线程数量很少。如果你发现大量线程阻塞在网络操作或是Sleep操作上,那就是问题, 需要修正。正如前面提到的那样,Goroutine是廉价的。更关键地是,如果它们在网络输入操作、Sleep操作、Channel操作或 sync包的原语操作上阻塞了,也不会导致承载其多路复用的线程阻塞。如果一个goroutine在上述某个操作上阻塞,Go运行时会调度另外一 个goroutine。即使成千上万的Goroutine被创建了出来,如果它们阻塞在上述的某个操作上,也不会浪费系统资源。从操作系统的视角来看,你的程序的行为就像是一个事件驱动的C程序似的。

四、最后的想法

就是这样,Goroutines可以并发的运行。不过和其他语言一样,组织两个或更多goroutine同时访问共享资源是很重要的。最好采用Channel在不同Goroutine间传递数据。

最后,虽然你无法直接控制Go运行时创建的线程的数量,但可以通过调用runtime.GOMAXPROCS(n)方法设置变量GOMAXPROCS来设 定使用的处理器核的数量。提高使用的处理器核数未必能提升你的程序的性能,这取决于程序的设计。程序剖析诊断工具(profiling tool)可以用来检查你的程序使用处理器核数的真实情况。

如发现本站页面被黑,比如:挂载广告、挖矿等恶意代码,请朋友们及时联系我。十分感谢! Go语言第一课 Go语言精进之路1 Go语言精进之路2 Go语言编程指南
商务合作请联系bigwhite.cn AT aliyun.com

欢迎使用邮件订阅我的博客

输入邮箱订阅本站,只要有新文章发布,就会第一时间发送邮件通知你哦!

这里是 Tony Bai的个人Blog,欢迎访问、订阅和留言! 订阅Feed请点击上面图片

如果您觉得这里的文章对您有帮助,请扫描上方二维码进行捐赠 ,加油后的Tony Bai将会为您呈现更多精彩的文章,谢谢!

如果您希望通过微信捐赠,请用微信客户端扫描下方赞赏码:

如果您希望通过比特币或以太币捐赠,可以扫描下方二维码:

比特币:

以太币:

如果您喜欢通过微信浏览本站内容,可以扫描下方二维码,订阅本站官方微信订阅号“iamtonybai”;点击二维码,可直达本人官方微博主页^_^:
本站Powered by Digital Ocean VPS。
选择Digital Ocean VPS主机,即可获得10美元现金充值,可 免费使用两个月哟! 著名主机提供商Linode 10$优惠码:linode10,在 这里注册即可免费获 得。阿里云推荐码: 1WFZ0V立享9折!


View Tony Bai's profile on LinkedIn
DigitalOcean Referral Badge

文章

评论

  • 正在加载...

分类

标签

归档



View My Stats