内容简介:在理解概念时参照了喵神的Swift 5 允许在 Release 构建过程中默认启用关于「独占访问内存」的运行时检查,进一步增强了 Swift 作为安全语言的能力。在 Swift 4 中,这种运行时检查仅允许在 Debug 构建过程中启用。在这篇文章中,首先我将解释这个变化对 Swift 开发人员的意义,然后再深入研究为什么它对 Swift 的安全和性能策略至关重要。为了实现内存安全,Swift 需要对变量进行独占访问时才能修改该变量。本质上来说,当一个变量作为
在理解概念时参照了喵神的 所有权宣言 - Swift 官方文章 Ownership Manifesto 译文评注版
Swift 5 允许在 Release 构建过程中默认启用关于「独占访问内存」的运行时检查,进一步增强了 Swift 作为安全语言的能力。在 Swift 4 中,这种运行时检查仅允许在 Debug 构建过程中启用。在这篇文章中,首先我将解释这个变化对 Swift 开发人员的意义,然后再深入研究为什么它对 Swift 的安全和性能策略至关重要。
背景
为了实现内存安全,Swift 需要对变量进行独占访问时才能修改该变量。本质上来说,当一个变量作为 inout
参数或者 mutating
方法中的 self
被修改时,不能通过不同的名称被访问的。
在以下示例中,通过将 count
作为 inout
参数传递来对 count
变量进行修改。出现独占性违规情况是因为 modifier
闭包对捕获的 count
变量同时进行了读取操作,并且在同一变量修改的范围内进行了调用。在 modifyTwice
函数中, count
变量只能通过 inout
修饰的 value
参数来进行安全访问而在 modified
闭包内,它只能以 $0
来进行安全访问。
func modifyTwice(_ value: inout Int, by modifier: (inout Int) -> ()) { modifier(&value) modifier(&value) } func testCount() { var count = 1 modifyTwice(&count) { $0 += count } print(count) } 复制代码
违反独占性的情况通常如此,程序员的意图此时显得有些模糊。他们希望 count
打印的值是「3」还是「4」呢?无论哪种结果,编译器都无法保证。更糟糕的是,编译器优化会在出现此类错误时产生微妙的不可预测行为。为了防止违反独占性并允许引入依赖于安全保证的语言特性,强制独占性最初在 Swift 4.0 中引入的: SE-0176:实施对内存的独占访问 。
编译时(静态)检测可以捕获许多常见的独占性违规行为,但是还需要运行时(动态)检测来捕获涉及逃逸闭包,类类型的属性,静态属性和全局变量的违规情况。Swift 4.0 同时提供了编译时和运行时的强制性检测,但运行时的强制检测仅在 Debug 构建过程中启用。
在 Swift 4.1 和 4.2 中,编译器检查能力逐渐得到加强,可以捕获到越来越多 程序员 绕过独占性规则的情况 —— 最明显的是在非逃逸闭包中捕获变量,或者将非逃逸闭包转换为逃逸闭包。Swift 4.2 宣称, 在 Swift 4.2 中将独占访问内存警告升级为错误 ,并解释了一些受新强制独占性检测影响的常见案例。
Swift 5 修复了语言模型中剩余的漏洞,并完全执行了该模型。 由于在 Release 编译过程中默认启用了对内存独占情况的强制性运行时检查,一些以前表现得很好的但未在 Debug 模式下被充分测试的 Swift 程序可能会受到一些影响.
一些罕见的还无法被编译器检测出来的涉及非法代码的情况(SR-8546, SR-9043 )。
对 Swift 项目的影响
Swift 5 中的强制独占性检查对现有项目可能会产生以下两种影响:
-
如果项目源码违反了 Swift 的独占性规则(具体查看 SE-0176:实施对内存的独占访问 ),Debug 调试测试时未能执行无效代码,然后,在构建 Release 二进制文件时可能会触发运行时陷阱。产生崩溃并抛出一个包含字符串的诊断消息:
「Simultaneous accesses to …, but modification requires exclusive access」
源代码级别修复通常很简单。后面的章节会展示常见的违规和修复示例。
-
内存访问检查的开销可能会影响的 Release 二进制包的性能。在大多数情况下,这种影响应该很小;如果你发现某个明显的性能下降情况,请提交 bug,以便我们了解需要改进的内容。作为一般性准则,应当避免在大多数性能关键循环中执行类属性访问,特别是在每个循环迭代中的不同对象上。如果必须如此,那么你可以将类属性修饰为
private
或internal
来帮助告知编译器没有其他代码访问循环内的相同属性。
你可以通过 Xcode 的「Exclusive Access to Memory」构建设置来禁用这些运行时检查,该设置还有「Run-time Checks in Debug Builds Only」和「Compile-time Enforcement Only」两个选项:
相对应的 swiftc 编译器标志是 -enforce-exclusivity = unchecked
和 -enforce-exclusivity = none
。
虽然禁用运行时检查可能会解决性能下降问题,但这并不意味着违反独占性是安全的。如果没有启用强制执行,程序员就必须承担遵守独占性规则的责任。强烈建议不要在构建 Release 包时禁用运行时检查,因为如果程序违反独占他性原则,则可能会出现不可预测的结果,包括崩溃或内存损坏。即使程序现在似乎能正常运行,未来的 Swift 版本也可能导致出现其他不可预测的情况,并且可能会暴露安全漏洞。
示例
在背景部分中的「testCount」示例中通过将局部变量作为 inout
参数来传递,与此同时在闭包中捕获它来违反了独占性原则。编译器在构建时检测到这一段时,就会如下面的屏幕截图所示:
通常可以通过添加 let
来简单地修复 inout
参数的违规情况:
let incrementBy = count modifyTwice(&count) { $0 += incrementBy } 复制代码
下一个示例可能会在 mutating
方法中同时修改 self
,从而产生异常。 append(removingFrom:)
方法通过删除另一个数组中所有元素来增加数组元素:
extension Array { mutating func append(removingFrom other: inout Array<Element>) { while !other.isEmpty { self.append(other.removeLast()) } } } 复制代码
但是,使用此方法将自身数组中的所有元素添加到自身将引发意外情况 —— 死循环。在这里,编译器在构建时再次抛出异常,因为「inout arguments are not allowed to alias each other」:
为了避免这些同时修改,可以将局部变量复制到另一个 var
中,然后作为 inout
参数传递给 mutating 方法:
var toAppend = elements elements.append(removingFrom: &toAppend) 复制代码
现在,这两个修改方法对不同的变量进行修改,所以没有产生冲突。
可以在 在 Swift 4.2 中将独占访问内存警告升级为错误 中找到导致构建错误的一些常见情况的示例。
通过更改第一个示例,使用全局变量而不是局部变量,可以防止编译器在构建时抛出错误。然而,运行程序会命中「Simultaneous access」的检查:
如示例中所示,在许多情况下,冲突访问发生在不同的语句中。
struct Point { var x: Int = 0 var y: Int = 0 mutating func modifyX(_ body:(inout Int) -> ()) { body(&x) } } var point = Point() let getY = { return point.y } // Copy `y`'s value into `x`. point.modifyX { $0 = getY() } 复制代码
运行时检测捕获了在开始调用 modifyX
时的访问信息,以及在 getY
闭包内发生冲突的访问信息,以及显示了导致冲突的堆栈信息:
Simultaneous accesses to ..., but modification requires exclusive access. Previous access (a modification) started at Example`main + .... Current access (a read) started at: 0 swift_beginAccess 1 closure #1 2 closure #2 3 Point.modifyX(_:) Fatal access conflict detected. 复制代码
Xcode 首先确定了内部访问冲突:
从侧边栏中当前线程的视图中选择「上一次访问」来确定外部修改:
通过复制闭包中所需要用的任何值,可以避免独占性违规:
let y = point.y point.modifyX { $0 = y } 复制代码
如果这是在没有 getter 和 setter 的情况下编写的:
point.x = point.y 复制代码
…那么就不存在独占性违规,因为在一个简单的赋值中(没有 inout
参数),修改是瞬间的。
在这一点上,读者可能想知道为什么在读写两个单独的属性时,原始示例被视为违反独占性规则; point.x
和 point.y
。因为 Point
被声明为 struct
,它被认为是一个值类型,这意味着它的所有属性都是整个值的一部分,访问任何一个属性都会访问整个值。当通过简单的静态分析可以证明安全性时,编译器会对此规则进行例外处理。 特别是,当同一语句发起对两个不相交存储的属性访问时,编译器会避免抛出违反独占性的报告。在下一个示例中,先调用 modifyX
的方法访问 point
,以便立即将其属性 x
作为 inout
传递。然后用相同的语句再次访问 point
,以便在闭包中捕获它。因为编译器可以立即看到捕获的值只用于访问属性 y
,所以没有错误。
func modifyX(x: inout Int, updater: (Int)->Int) { x = updater(x) } func testDisjointStructProperties(point: inout Point) { modifyX(x: &point.x) { // First `point` access let oldy = point.y // Second `point` access point.y = $0; // ...allowed as an exception to the rule. return oldy } } 复制代码
属性可以分为三类:
-
值类型的实例属性
-
引用类型的实例属性
-
任意类型的静态和类属性
只有对第一类属性(实例属性)的修改才会要求对聚合值的整体存储具有独占性访问,如上面的 struct Point
示例所示。另外两种类别可以作为独立存储分别执行。 如果这个例子被转换成一个类对象,那么将不会违反独占性原则:
class SharedPoint { var x: Int = 0 var y: Int = 0 func modifyX(_ body:(inout Int) -> ()) { body(&x) } } var point = SharedPoint() let getY = { return point.y } // no longer a violation when called within modifyX // Copy `y`'s value into `x`. point.modifyX { $0 = getY() } 复制代码
目的
上述编译时和运行时独占性检查的结合对于加强 Swift 的内存安全 是很必要的。完全执行这些规则,而不是让程序员承担遵守独占性规则的负担,至少有以下五种帮助:
-
执行独占性检查消除了程序涉及可变状态和远距离动作的危险交互。
随着程序规模的不断扩大,越来越可能会以意想不到的方式进行交互。下面的例子在类似于上面的
Array.append(removedFrom:)
例子,需要执行独占性检查来避免程序员将相同的变量同时作为源数据和目标数据进行传递。但请注意,一旦涉及到类对象,因为这两个变量引用了同一个对象,程序就会在无意中更容易在src
和dest
位置上传递同一个的Names
实例。当然,这样就会导致死循环:
func moveElements(from src: inout Set<String>, to dest: inout Set<String>) { while let e = src.popFirst() { dest.insert(e) } } class Names { var nameSet: Set<String> = [] } func moveNames(from src: Names, to dest: Names) { moveElements(from: &src.nameSet, to: &dest.nameSet) } var oldNames = Names() var newNames = oldNames // Aliasing naturally happens with reference types. moveNames(from: oldNames, to: newNames) 复制代码
SE-0176:实施对内存的独占访问 更深入地描述了这个问题。
-
执行独占性检查消除了语言中未指定的行为规则。
在 Swift 4 之前,独占性对于明确定义的程序行为是必要的,但规则是不受限制的。在实践中,人们很容易以微妙的方式违反这些规则,使程序容易受到不可预测的行为的影响,特别是在编译器的各个发布版本中。
-
执行独占性检查是稳定 ABI 的必要条件。
未能完全执行独占性检查将会对 ABI 的稳定性产生不可预测的影响。在没有进行完全检查的情况下构建的现有二进制文件可能在某一个版本中能够正常运行,但在未来的编译器版本、标准库和运行时中无法正确运行。
-
执行独占性检查使性能优化更合法,同时保护内存安全。
对
inout
参数和mutating
方法的独占性检查向编译器提供了重要信息,可用于优化内存访问和引用计数操作。如上面第2点所述,简单地声明一个未指定的行为规则对于编译器来说是不够,因为 Swift 是一种内存安全语言。完全强制执行独占性检查允许编译器基于内存独占性进行优化,而不会牺牲内存安全性。 -
独占性规则为程序员提供所有权和仅移动类型的控制权。
以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,也希望大家多多支持 码农网
猜你喜欢:- 图解AQS系列(上)--独占锁
- 解读 JUC —— AQS 独占模式
- XPath 1.0独占或节点集表达式
- JDK并发之独占锁ReentrantLock以及Condition
- golang的强制类型转换
- Lucene 段的强制合并(一)
本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。