美文网首页
Swift中内存安全之独占性原则

Swift中内存安全之独占性原则

作者: llllllllIllllIl | 来源:发表于2019-11-15 18:56 被阅读0次

    概念:

    所谓独占性原则,是指:

    如果一个存储引用表达式的求值结果是一个由变量所实现的存储引用,那么对这个引用的访问的持续时间段,不应该与其他任何对这个相同变量的访问持续时间段产生重合,除非这两个访问都是读取访问。(不应该出现重入访问,Swift只能基本上禁止重(再)入式的访问)而且独占性原则会保证在访问期间不会有其他访问能对原来的变量进行修改。

    这里有一个地方故意说得比较模糊:这条原则只指出了访问“不应该”产生重合,但是它没有指出如何强制做到这一点。这是因为我们将对不同类型的存储使用不同的机制来强制检测独占性原则。

    独占性原则的强制适用机制: 想要让独占性原则适用,我们有三种可行机制:静态强制,动态强制,以及未定义强制。所要选择的机制必须能由存储声明简单地决定,因为存储的定义和对它的所有直接的访问方法都必须满足声明的要求。一般来说,我们通过存储被声明为的类型,它的容器 (如果存在的话,值类型、引用类型、static变量),以及任何存在的声明标记 (比如 var 或者 inout 之类) 来决定适用哪种机制。


    如果一个访问不可能在其访问期间被其它代码访问,那么就是一个瞬时访问。正常来说,两个瞬时访问是不可能同时发生的。大多数内存访问都是瞬时的。像+等运算符函数是长期访问,不是瞬时访问。

    重叠访问(独占性原则,独占访问)主要是出现在inout函数以及方法或者值类型的mutating方法、或者对值类型(的属性)重叠访问。此类情况我们称它为长期访问,或者内存的重叠访问、独占访问。而引用类型的重叠访问,

    重入错误: 【长期访问时禁止变量或值(的属性)重入,出现同时访问同一个内存地址的读写冲突。】,其实inout和mutating都是处理重入问题的手段和规则,只有代码写的出格了才会出现重入错误。对于引用类型和静态变量的访问,必须遵守动态强制的原则,检测出那些必然会违反独占性原则的情况。

    如何保证独占性: 动态的修改,对于性能的损失是不可忽视的,在尽可能的情况下使用静态的方法来保证独占性。只有在确实无法静态决定的情况下,再使用动态方式。


    技术总结: 限制结构体属性的重叠访问对于保证内存安全不是必要的。保证内存安全是必要的,但因为访问独占权的要求比内存安全还要更严格——意味着即使有些代码违反了访问独占权的原则,也是内存安全的,所以如果编译器可以保证这种非专属的访问是安全的,那 Swift 就会允许这种行为的代码运行。特别是当你遵循下面的原则时,它可以保证结构体属性的重叠访问是安全的:

    • 你访问的是实例的存储属性,而不是计算属性或类的属性

    • 结构体是本地变量的值,而非全局变量

    • 结构体要么没有被闭包捕获,要么只被非逃逸闭包捕获了。不管什么时候,程序员应尽量避免对值类型的访问出现重入。

    • 总的原则: 而在某个特定 (但是很重要) 的特殊情况以外(mutating时可以修改self或者结构体是本地变量并且访问的是实例的存储属性),总是将值类型的属性当作是非独立的来进行处理(独占原则,不能将对不同的属性的访问和对整体值的访问独立开来)。

    • 引用类型属性和 static 属性看作各自独立的属性

    如果编译器无法保证访问的安全性,它就不会允许那次访问。


    想要维持变量的不变几乎是不可能的,所以Swift基本上禁止重(再)入式的访问,但也不是完全可能的,只能尽可能地帮助规范程序员们的代码书写(提出inout、mutating等规则保证内存安全并尽可能提高性能)。而且我们也要在运行时做一些额外的工作,来确保这样的代码不会导致未定义的行为,或者是让整个进程发生错误。


    注意

    如果你写过并发和多线程的代码,内存访问冲突也许是同样的问题。然而,这里访问冲突的讨论是在单线程的情境下讨论的,并没有使用并发或者多线程。

    如果你曾经在单线程代码里有访问冲突,Swift 可以保证你在编译或者运行时会得到错误。对于多线程的代码,可以使用 Thread Sanitizer 去帮助检测多线程的冲突。

    代码示例:

    ️长期访问中出现了变量的重入,stepSize变量进入了2次。inout规则

    ⭐️关于变量重入和捕获变量的讨论: 。【inout可以捕获并可选的修改变量,也可以不使用inout直接通过闭包去捕获变量】,暂时未支持在函数外用inout进行捕获【类似 inout root = &tree.root】,只能使用闭包去捕获变量,但很麻烦。


    var stepSize = 1
    func increment(_ number: inout Int) {
        number += stepSize
    }
    increment(&stepSize)
    // Error: conflicting accesses to stepSize
    

    ️长期访问中(输入输出形式参数函数中)多次重入相同的输入输出形式参数。

    func balance(_ x: inout Int, _ y: inout Int) {
        let sum = x + y
        x = sum / 2
        y = sum - x
    }
    var playerOneScore = 42
    var playerTwoScore = 30
    balance(&playerOneScore, &playerTwoScore)  // OK
    balance(&playerOneScore, &playerOneScore)
    // Error: conflicting accesses to playerOneScore
    

    ️mutating方法重入self

    struct Player {
        var name: String
        var health: Int
        var energy: Int
        static let maxHealth = 10
        mutating func restoreHealth() {
            health = Player.maxHealth
        }
    }
    
    extension Player {
        mutating func shareHealth(with teammate: inout Player) {
            balance(&teammate.health, &health)
        }
    }
    var oscar = Player(name: "Oscar", health: 10, energy: 10)
    var maria = Player(name: "Maria", health: 5, energy: 10)
    oscar.shareHealth(with: &maria)  // OK
    oscar.shareHealth(with: &oscar)
    // Error: conflicting accesses to oscar
    

    ️元组(的属性)适用独占性原则,不能独立访问。(元组也是值类型)

    var playerInformation = (health: 10, energy: 20)
    balance(&playerInformation.health, &playerInformation.energy)
    // Error: conflicting access to properties of playerInformation
    

    ️在某个特定 (但是很重要) 的特殊情况以外(mutating时可以修改self或者结构体是本地变量并且访问的是实例的存储属性),总是将值类型(的属性)当作是非独立的来进行处理(独占原则)。

    var holly = Player(name: "Holly", health: 10, energy: 10)
    balance(&holly.health, &holly.energy)  // Error
    

    ️特殊情况下,编译器可以保证对结构体属性的重叠访问是安全的,那 Swift 就会允许这种行为的代码运行。我们通过3个特别的原则进行判断重叠访问是否是内存安全的。

    • 你访问的是实例的存储属性,而不是计算属性或类的属性
    • 结构体是本地变量的值,而非全局变量
    • 结构体要么没有被闭包捕获,要么只被非逃逸闭包捕获了
    func someFunction() {
        var oscar = Player(name: "Oscar", health: 10, energy: 10)
        balance(&oscar.health, &oscar.energy)  // OK
    }
    

    通过下标访问值类型的一个部分和访问整个值是同等对待的(再入式访问),包括同一个数组内两个不同的数组元素进行同时(重叠)访问都不是内存安全的。

    这会妨碍到某些操作数组时的通常做法,不过有些 (比如并行地改变一个数组的不同切片这类) 事情在 Swift 中本来就充满了问题。我们认为,通过有目的的对集合的 API 进行改进,可以将缓和所带来的主要影响。对于数组的操作可能是并行编程中比较常见的多线程问题。在很大程度上,下标操作和实例属性的访问类似,我们可以通过加锁或者 GCD 做 barrier 的方式来确保数组线程安全。(Swift中数组不是线程安全的)

    ️总是将引用类型属性和 static 属性看作各自独立的属性,不会有重叠访问的问题,但swift暂时不支持原子操作,所以多线程中处理这类属性时需要加锁。

    ️ 闭包中的嵌套式访问,如何处理这种同一个变量再入式的访问。想要维持变量的不变几乎是不可能的,所以Swift基本上禁止重(再)入式的访问,但也不是完全可能的,只能尽可能地帮助规范程序员们的代码书写(提出inout、mutating等规则保证内存安全并尽可能提高性能)。而且我们也要在运行时做一些额外的工作,来确保这样的代码不会导致未定义的行为,或者是让整个进程发生错误。

    如果闭包 C 不会逃逸出函数,闭包predicate的调用会被静态强制认为是试图对 某个变量V(Self) 进行写操作的调用,同时也会被视作对 某个变量V(Self) 的读取操作。那么这样的再入访问就有可能发生修改,需要程序员有意识的假设会出现修改和预防修改,可以使用值语义进行复制,或者深拷贝。如果闭包 C 有可能逃逸,必须遵守动态强制的原则,除非所有的访问都是读取访问。

    extension Array {
        mutating func organize(_ predicate: (Element) -> Bool) {
          let first = self[0]
          if !predicate(first) { return }
          ...
          // something here uses first
        }
      }
    

    不过,如果我们能够支持唯一引用的 class 类型的话,独占性原则就可以静态地适用它们的属性了。(暂时Swift还未支持)

    参考文献

    OwnershipManifesto.
    onevcat
    Swift教程
    Swift中文教程

    相关文章

      网友评论

          本文标题:Swift中内存安全之独占性原则

          本文链接:https://www.haomeiwen.com/subject/wbgsictx.html