今天测试用Go语言写的角色服务器,发现在模拟大量客户端获取角色列表的时候会卡住,但是服务器程序的CPU占用率为零。分析并经过代码检查确认是goroutine死锁。
此问题涉及到的代码主要是一个database类,封装了角色数据的一些数据库操作。虽然sql.DB后台支持连接缓冲池,但是为了限制资源的使用,我仍然在database类里做了并发数的限制,角色数据的每个操作必须先获得令牌才能进行后续操作,操作完成后归还令牌。代码主要结构如下所示:
import "database/sql"
const DBCONN_MAXCOUNT = 128
type database struct {
db *sql.DB
tokens chan int
}
func (p *database) Open(source string) error {
db, err := sql.Open("mysql", source) if err != nil {
return err
} p.db = db p.tokens = make(chan int, DBCONN_MAXCOUNT) for i := 0; i < DBCONN_MAXCOUNT; i++ {
p.tokens <- 1
} return nil
}
func (p *database) Close() {
p.db.Close()
}
func (p *database) GetRoleData(name string) ([]byte, error) {
token := <-p.tokens defer func() {p.tokens<-token}() //相关数据库操作...
}
func (p *database) DeleteRole(name string) error {
token := <-p.tokens defer func() {p.tokens<-token}() //相关数据库操作...
}
BUG的原因是在DeleteRole函数里其实进行了一些数据库操作,包括为了检查角色数据是否需要放到回收库需要先获取角色数据检查角色的等级等。其中获取角色数据就调用到了GetRoleData函数。这就导致一种可能性:如果所有客户端都执行到DeleteRole函数里,准备执行GetRoleData函数时没有可用令牌,就会集体陷入死锁。
解决办法是定义了一个不需要更底层的函数getRoleData,供GetRoleData和DeleteRole函数调用,从而不需要申请多个令牌。
func (p *database) getRoleData(name string) ([]byte, error) {
//相关数据库操作...
}
func (p *database GetRoleData(name string) ([]byte, error) {
token := <-p.tokens defer func() {p.tokens<-token}() return p.getRoleData(name)
}
func (p *database) DeleteRole(name string) error {
token := <-p.tokens defer func() {p.tokens<-token}() data, err := p.getRoleData(name) if err != nil {
return err
} //其他操作...
}
总结:
1.最好不要设计成做一件事情需要获取N个资源,然后依次去申请。如果不能避免申请多个资源,也应该按照固定次序去申请,否则会导致死锁(本例中是不小心导致的,因为申请令牌是后加的)。这是所有编程语言设计并发程序的通用规则。
2.函数分级。一个类(结构)的暴露给类(结构)外部使用的函数最好不要相互调用。如果有共用代码,可以提取为更底层的一个内部函数,供给多个上一级的函数使用。这个不处理好容易导致以后调整功能需要修改代码时出问题。