美文网首页
24 Memory Safety 内存安全

24 Memory Safety 内存安全

作者: 微笑中的你 | 来源:发表于2019-04-05 23:01 被阅读0次

    默认情况下,Swift可以防止代码中发生不安全行为。例如,Swift确保变量在使用前被初始化,内存被释放后不会被访问,并且检查数组索引是否有越界错误。

    Swift还要求修改内存中某个位置的代码具有对该内存的独占访问权,从而确保对同一内存区域的多次访问不会发生冲突。因为Swift是自动管理内存的,所以大多数时候你根本不用考虑访问内存。但是,了解可能发生冲突的地方很重要,这样您就可以避免编写访问内存冲突的代码。如果您的代码确实包含冲突,则会得到编译时或运行时错误。

    Understanding Conflicting Access to Memory

    当您设置变量的值或将参数传递给函数时,对内存的访问发生在代码中。例如,下面的代码包含读访问和写访问:

    // 对存储1的内存进行写访问
    var one = 1
    
    //对存储1的内存进行读访问
    print("We're number \(one)!")
    

    当代码的不同部分试图同时访问内存中的相同位置时,可能会发生对内存的冲突访问。同时多次访问内存中的某个位置可能会产生不可预测或不一致的行为。在Swift中,有几种方法可以修改跨越几行代码的值,从而使在修改过程中访问值成为可能。

    memory_shopping_2x.png

    当您向预算添加项目时,它处于临时的无效状态,因为总金额没有更新以反映新添加的项目。在添加项目的过程中读取总金额会给出不正确的信息。

    这个示例还演示了在修复对内存的冲突访问时可能遇到的一个挑战:有时有多种方法可以修复产生不同答案的冲突,而且并不总是很明显哪个答案是正确的。在本例中,根据需要原始总额还是更新后的总额,正确的答案可能是5或320。在修复冲突访问之前,您必须确定它的目的是什么。

    如果您已经编写了并发或多线程代码,那么对内存的冲突访问可能是一个常见的问题。然而,这里讨论的冲突访问可能发生在单个线程上,并且不涉及并发或多线程代码。

    如果你在一个线程中有冲突的内存访问,Swift保证你会在编译时或运行时得到一个错误。对于多线程代码,使用线程杀毒器来帮助检测线程之间的冲突访问。

    Characteristics of Memory Access 内存访问特性

    在访问冲突的上下文中,需要考虑内存访问的三个特征:访问是读还是写、访问的持续时间和正在访问的内存中的位置。具体地说,如果您有两个符合以下所有条件的访问,则会发生冲突:

    • 至少有一个是写访问。
    • 它们访问内存中的相同位置。
    • 他们的时间重叠。

    读访问和写访问之间的区别通常很明显:写访问会更改内存中的位置,而读访问不会。内存中的位置指的是正在访问的内容——例如,变量、常量或属性。内存访问的持续时间可以是瞬时的,也可以是长期的。

    如果其他代码无法在访问开始之后而在访问结束之前运行,则访问是瞬时的。从本质上讲,两个瞬时访问不可能同时发生。大多数内存访问都是瞬时的。例如,下面代码清单中的所有读写访问都是瞬时的:

    func oneMore(than number: Int) -> Int {
        return number + 1
    }
    
    var myNumber = 1
    myNumber = oneMore(than: myNumber)
    print(myNumber)
    // Prints "2"
    

    然而,有几种访问内存的方法,称为长期访问,它们跨越了其他代码的执行。瞬时访问和长期访问的区别在于,其他代码可能在长期访问开始后运行,但在长期访问结束之前运行,这称为重叠。长期访问可以与其他长期访问和瞬时访问重叠。

    重叠访问主要出现在在函数和方法中使用in-out参数的代码中,或者在结构的方法中进行修改的代码中。下面将讨论使用长期访问的特定类型的Swift代码。

    Conflicting Access to In-Out Parameters 输入输出参数的访问冲突

    函数具有对其所有in-out参数的长期写访问权。in-out参数的写访问在所有非in-out参数被评估之后开始,并持续到函数调用的整个期间。如果有多个in-out参数,那么写入访问将按照参数出现的顺序开始。

    这种长期写访问的一个结果是,您不能访问作为in-out传递的原始变量,即使范围规则和访问控制允许这样做—对原始变量的任何访问都会产生冲突。例如:

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

    在上面的代码中,stepSize是一个全局变量,通常可以从increment(_:)中访问它。但是,对stepSize的读访问与对number的写访问重叠。如下图所示,number和stepSize都指向内存中的相同位置。读和写访问指的是相同的内存,它们重叠,产生冲突。

    memory_increment_2x.png

    解决这个冲突的一个方法是显式复制stepSize:

    // Make an explicit copy.
    var copyOfStepSize = stepSize
    increment(&copyOfStepSize)
    
    // Update the original.
    stepSize = copyOfStepSize
    // stepSize is now 2
    

    当您在调用increment(_:)之前复制stepSize时,很明显copyOfStepSize的值是由当前的步骤大小递增的。读访问在写访问开始之前结束,因此不存在冲突。

    对in-out参数的长期写访问的另一个结果是,将单个变量作为同一函数的多个in-out参数的参数传递会产生冲突。例如:

    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
    

    上面的balance(::)函数修改了它的两个参数,以便在它们之间平均分配总价值。用playerOneScore和playerTwoScore作为参数调用它不会产生冲突——有两个写访问在时间上重叠,但是它们访问内存中的不同位置。相反,将playerOneScore作为两个参数的值传递会产生冲突,因为它试图同时执行对内存中相同位置的两次写访问。

    因为操作符是函数,所以它们也可以长期访问它们的in-out参数。例如,如果balance(::)是一个名为<^>的操作符函数,那么编写playerOneScore <^> playerOneScore将导致与balance(&playerOneScore, &playerOneScore)相同的冲突。

    Conflicting Access to self in Methods 方法中对self的冲突访问

    结构上的修改方法在方法调用期间具有对self的写访问权。例如,考虑一个游戏,每个玩家都有一个生命值(在受到伤害时降低)和一个能量值(在使用特殊技能时降低)。

    struct Player {
        var name: String
        var health: Int
        var energy: Int
    
        static let maxHealth = 10
        mutating func restoreHealth() {
            health = Player.maxHealth
        }
    }
    

    在上面的restoreHealth()方法中,对self的写访问从方法的开头开始,一直持续到方法返回。在本例中,restoreHealth()中没有其他代码可以对Player实例的属性进行重叠访问。下面的shareHealth(with:)方法将另一个Player实例作为in-out参数,从而创建了重叠访问的可能性。

    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:)方法来与Maria的玩家共享health不会引起冲突。在方法调用期间有对oscar的写访问,因为oscar是在一个可变方法中self的值,在相同的时间内有对maria的写访问,因为maria是作为in-out参数传递的。如下图所示,它们访问内存中的不同位置。即使这两个写访问在时间上重叠,它们也不会冲突。

    memory_share_health_maria_2x.png

    但是,如果将oscar作为参数传递给shareHealth(with:),就会产生冲突:

    oscar.shareHealth(with: &oscar)
    // Error: conflicting accesses to oscar
    

    在方法的持续时间内,修改方法需要对self的写访问权,而in-out参数在相同的持续时间内需要对teamate的写访问权。在方法中,self和team都引用内存中的相同位置,如下图所示。这两个写访问指的是相同的内存,它们重叠,产生冲突。

    memory_share_health_oscar_2x.png

    Conflicting Access to Properties 属性访问冲突

    结构、元组和枚举等类型由单独的组成值组成,例如结构的属性或元组的元素。因为这些是值类型,所以对值的任何部分进行修改都会对整个值进行修改,这意味着对其中一个属性的读或写访问需要对整个值进行读或写访问。例如,对元组元素的重叠写访问会产生冲突:

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

    在上面的例子中,对元组的元素调用balance(::)会产生冲突,因为对playerInformation有重叠的写访问。playerInformation.health 和 playerInformation.energy作为in-out参数传递,这意味着balance(::)在函数调用期间需要对它们进行写访问。在这两种情况下,对元组元素的写访问都需要对整个元组的写访问。这意味着有两种对playerInformation的写访问,它们的持续时间重叠,从而导致冲突。

    下面的代码显示,对存储在全局变量中的结构的属性的重叠写访问也会出现相同的错误。

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

    实际上,对结构属性的大多数访问都可以安全地重叠。例如,如果将上面例子中的变量holly改为局部变量而不是全局变量,编译器可以证明对结构存储属性的重叠访问是安全的:

    func someFunction() {
        var oscar = Player(name: "Oscar", health: 10, energy: 10)
        balance(&oscar.health, &oscar.energy)  // OK
    }
    

    在上面的例子中,Oscar的健康和精力被传递为两个In -out参数来平衡(::)。编译器可以证明内存安全得到了保护,因为这两个存储的属性没有以任何方式交互。

    限制对结构属性的重叠访问并不总是保持内存安全所必需的。内存安全是需要的保证,但是独占访问比内存安全要求更严格——这意味着一些代码保留内存安全,即使它违反了对内存的独占访问。如果编译器能够证明对内存的非排他性访问仍然是安全的,Swift就允许使用这种内存安全代码。具体来说,如果符合以下条件,则可以证明对结构属性的重叠访问是安全的:

    • 您只访问实例的存储属性,而不访问计算属性或类属性。
    • 结构是局部变量的值,而不是全局变量。
    • 结构要么不被任何闭包捕获,要么只被非转义闭包捕获。

    如果编译器不能证明访问是安全的,它就不允许访问。

    <<返回目录

    相关文章

      网友评论

          本文标题:24 Memory Safety 内存安全

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