文章摘要
文章核心内容:内存安全与线程安全密不可分,单纯将内存安全与线程安全区分开来并不实用。作者通过Go语言示例说明,即使语言本身被认为是内存安全的,线程不安全仍可能导致未定义行为,因此真正的目标是确保程序没有未定义行为。
文章总结
标题:没有线程安全,就没有内存安全
近年来,内存安全成为了热门话题。但究竟什么是内存安全?这个问题比想象中更难回答。通常,人们用这个词来指代那些确保程序不会出现“释放后使用”或“越界访问”等内存问题的编程语言。然而,这种定义往往与线程安全等其他安全概念区分开来,线程安全指的是程序不会出现某些并发错误。本文认为,这种区分并不实用,我们真正希望程序具备的特性是避免未定义行为(Undefined Behavior)。
数据竞争如何破坏内存安全
将安全细分为内存安全和线程安全等类别的主要问题在于,线程不安全的语言无法真正提供内存安全。以Go语言为例,Go被认为是内存安全的语言,但以下程序却会迅速崩溃:
```go package main
type Thing interface { get() int }
type Int struct { val int } func (s *Int) get() int { return s.val }
type Ptr struct { val *int } func (s *Ptr) get() int { return *s.val }
var globalVar Thing = &Int { val: 42 }
func repeat_get() { for { x := globalVar x.get() } }
func repeat_swap() { var myval = 0 for { globalVar = &Ptr { val: &myval } globalVar = &Int { val: 42 } } }
func main() { go repeatget() repeatswap() } ```
运行该程序时,它会因访问地址0x2a(即十进制的42)而崩溃。这是因为Go在更新接口类型的全局变量时,会分别更新数据指针和虚表指针,导致在某些情况下读取到不一致的值,从而引发未定义行为。
其他语言的情况
你可能会问,其他语言是否也存在类似问题?Java也允许数据竞争,但Java开发者通过引入并发内存模型,确保即使存在数据竞争,程序也不会出现未定义行为。Java程序可能会看到某些变量的意外旧值,但永远不会出现解引用无效指针的情况。因此,Java程序在某种程度上是线程安全的。
语言设计的两种选择
为了确保并发不会破坏基本的不变性,语言设计通常有两种选择:
- 确保任意并发程序在某种意义下行为“合理”:这需要付出较大代价,限制语言不能假设多字值的一致性,并限制编译器的优化能力。Java、C#、OCaml、JavaScript和WebAssembly等语言采用了这种策略。
- 通过强大的类型系统完全排除大多数访问的数据竞争:Rust和Swift采用了这种策略,通过类型系统处理并发问题。
Go则选择了第三种方式,既不确保并发程序的合理性,也不通过类型系统排除数据竞争。因此,严格来说,Go并不是内存安全的语言。尽管Go提供了检测数据竞争的工具,但在实际程序中,开发者仍需依赖测试覆盖所有可能的情况,这恰恰是强类型系统和静态安全保证旨在避免的问题。
结论
本文认为,人们谈论内存安全时,真正关心的是程序不会破坏语言本身。未定义行为的出现意味着程序违背了语言的基本抽象,这为安全漏洞提供了温床。因此,安全语言与不安全语言之间的分界线在于程序是否可能出现未定义行为,而不是进一步细分为内存安全、线程安全等类别。
尽管Go在安全谱系上更接近安全语言,但它仍然存在由数据竞争引发的未定义行为。理解语言提供的安全保证及其局限性,对于开发者至关重要。希望本文能帮助读者更好地理解不同语言设计选择带来的非平凡后果。
评论总结
评论主要围绕Go语言的内存安全性和线程安全性展开,观点多样且相互补充。
Go语言的内存安全性
- 支持观点:Go语言在内存安全方面表现良好,几乎没有内存损坏漏洞的利用案例。
- 引用:"The fact is that Go doesn't admit memory corruption vulnerabilities, and the way you know that is the fact that there are practically zero exploits for memory corruption vulnerabilities targeting pure Go programs."
- 引用:"Go lacks that. It seems to be a rare problem in practice, but if you want guarantees, Go doesn't give you them."
- 反对观点:Go语言在某些情况下可能存在线程安全问题,尤其是在并发访问共享变量时。
- 引用:"Go lacks that. It seems to be a rare problem in practice, but if you want guarantees, Go doesn't give you them."
- 支持观点:Go语言在内存安全方面表现良好,几乎没有内存损坏漏洞的利用案例。
线程安全性与内存安全性的关系
- 支持观点:线程安全是内存安全的前提,尤其是在多线程环境中。
- 引用:"I agree with the author's claim that you need thread safety for memory safety."
- 反对观点:线程不安全并不一定导致内存不安全,某些未定义行为不会影响内存安全。
- 引用:"There is plenty of undefined behavior that can't lead to violating memory safety."
- 支持观点:线程安全是内存安全的前提,尤其是在多线程环境中。
Go语言的并发模型
- 支持观点:Go语言强调使用通信原语而非直接共享内存,这有助于减少并发问题。
- 引用:"To be fair though, go has a big emphasis on using its communication primitives instead of directly sharing memory between goroutines."
- 反对观点:默认的全局变量和共享内存访问是数据损坏和竞争条件的根源,进程模型可能更好。
- 引用:"The sad thing is that most languages with threads have a default of global variables and unrestricted shared memory access."
- 支持观点:Go语言强调使用通信原语而非直接共享内存,这有助于减少并发问题。
实际应用中的问题
- 支持观点:尽管Go语言存在潜在的并发问题,但在实际生产中并不常见。
- 引用:"In many years of using Go in production I don't think I've ever come across a situation where the exact requirements to cause this bug have occurred."
- 反对观点:Go语言在某些情况下允许撕裂写入(torn writes),这可能带来性能问题。
- 引用:"For some performance hit, though, the torn writes problem could just be fixed."
- 支持观点:尽管Go语言存在潜在的并发问题,但在实际生产中并不常见。
总结:评论者对Go语言的内存安全性和线程安全性持有不同看法,尽管Go在实际应用中表现良好,但其并发模型和潜在的线程安全问题仍引发讨论。