使用Semgrep发现Goroutine泄漏
Goroutine泄漏解析
在Go语言中,虽然使用go
关键字就能轻松实现异步函数调用,但编写并发代码仍然存在风险。其中,goroutine泄漏是一种常见但很少被讨论的并发bug,它通常在使用通道进行goroutine间消息同步时出现错误导致。
goroutine泄漏通常发生在错误使用无缓冲通道的情况下,当应该使用缓冲通道时却使用了无缓冲通道。这类bug会导致goroutine挂起在内存中,最终耗尽系统资源,造成系统崩溃或拒绝服务状况。
实际示例分析
|
|
在上述代码中,第21行的通道写操作会阻塞包含它的匿名goroutine。第19行声明的goroutine将被阻塞,直到在dataChan上发生读操作。这是因为使用无缓冲通道时,读写操作都会阻塞goroutine,每个写操作都必须有相应的读操作。
导致匿名goroutine泄漏的两种场景:
- 如果第24行的读操作之前发生了第二个case(
case <- time.After(timeout)
),requestData函数将退出,其中的匿名goroutine将被泄漏 - 如果两个case同时触发,调度器将随机选择其中一个case。如果选择了第二个case,匿名goroutine将被泄漏
运行代码时将得到以下输出:
|
|
挂起的goroutine就是第19行的匿名goroutine。
解决方案
使用缓冲通道可以修复上述问题。只需在第17行的通道声明中添加长度:
|
|
运行更新后的程序可以确认不再有挂起的goroutine。
发现bug
虽然调试并发问题很复杂,但通过仔细检查野外发现的常见Go并发bug,我们可以识别出可用于创建Semgrep规则的模式。
在使用Semgrep之前,需要认识到其局限性。在搜索并发bug时,最主要的限制是Semgrep无法进行过程间分析,这意味着我们需要针对包含在单个函数中的bug。
Semgrep规则构建
基于goroutine泄漏的典型表现构建Semgrep规则:
- 声明了类型为T的无缓冲通道C
- 在匿名goroutine G中执行对通道C的写/发送操作
- 在select块(或G之外的另一个位置)读取/接收C
- 程序遵循在封闭函数终止前不发生C的读操作的执行路径
最后一步通常会导致goroutine泄漏。
检测模式
要检测示例中的goroutine泄漏,可以使用以下模式:
|
|
这些模式将帮助我们识别代码中的无缓冲通道声明,从而发现潜在的goroutine泄漏问题。