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

概念:

所谓独占性原则,是指:

如果一个存储引用表达式的求值结果是一个由变量所实现的存储引用,那么对这个引用的访问的持续时间段,不应该与其他任何对这个相同变量的访问持续时间段产生重合,除非这两个访问都是读取访问。(不应该出现重入访问,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中文教程

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 219,539评论 6 508
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 93,594评论 3 396
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 165,871评论 0 356
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,963评论 1 295
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,984评论 6 393
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,763评论 1 307
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,468评论 3 420
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,357评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,850评论 1 317
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 38,002评论 3 338
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 40,144评论 1 351
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,823评论 5 346
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,483评论 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 32,026评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,150评论 1 272
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,415评论 3 373
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 45,092评论 2 355