zoukankan      html  css  js  c++  java
  • Swift5.3 语言指南(二十七) 内存安全

    ★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★
    ➤微信公众号:山青咏芝(shanqingyongzhi)
    ➤博客园地址:山青咏芝(https://www.cnblogs.com/strengthen/
    ➤GitHub地址:https://github.com/strengthen/LeetCode
    ➤原文地址:https://www.cnblogs.com/strengthen/p/9739940.html 
    ➤如果链接不是山青咏芝的博客园地址,则可能是爬取作者的文章。
    ➤原文已修改更新!强烈建议点击原文地址阅读!支持作者!支持原创!
    ★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★

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

    通过要求修改内存中位置的代码具有对该内存的独占访问权限,Swift还确保对同一内存区域的多次访问不冲突。由于Swift会自动管理内存,因此大多数时候您根本不需要考虑访问内存。但是,重要的是要了解在何处可能发生冲突,因此可以避免编写对内存的访问有冲突的代码。如果您的代码中确实包含冲突,则会出现编译时或运行时错误。

    了解对内存的访问冲突

    当您执行诸如设置变量的值或将参数传递给函数之类的操作时,就会在代码中访问内存。例如,以下代码包含读取访问权限和写入访问权限:

    1. // A write access to the memory where one is stored.
    2. var one = 1
    3. // A read access from the memory where one is stored.
    4. print("We're number (one)!")

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

    通过考虑如何更新写在纸上的预算,您可以看到类似的问题。更新预算分为两个步骤:首先添加项目的名称和价格,然后更改总金额以反映当前列表中的项目。在更新前后,您可以从预算中读取任何信息并获得正确的答案,如下图所示。

    ../_images/memory_shopping_2x.png

    在将项目添加到预算时,它处于临时无效状态,因为尚未更新总金额以反映新添加的项目。在添加项目的过程中读取总金额会给您错误的信息。

    此示例还演示了在解决冲突的内存访问时可能遇到的挑战:有时有多种方法可以解决冲突,从而产生不同的答案,并且哪个答案正确并不总是很明显。在此示例中,根据您是要原始总金额还是更新后的总金额,$ 5或$ 320可能是正确的答案。在解决冲突访问之前,您必须确定打算执行的操作。

    注意

    如果您编写了并发或多线程代码,则对内存的访问冲突可能是一个熟悉的问题。但是,这里讨论的冲突访问可以在单个线程上发生,并且涉及并发或多线程代码。

    如果您在单个线程中对内存的访问存在冲突,Swift保证您将在编译时或运行时收到错误。对于多线程代码,请使用Thread Sanitizer帮助检测跨线程的冲突访问。

    内存访问的特征

    在冲突的访问环境中要考虑内存访问的三个特征:访问是读还是写,访问的持续时间以及要访问的内存位置。具体来说,如果您具有两个满足以下所有条件的访问权限,则会发生冲突:

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

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

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

    1. func oneMore(than number: Int) -> Int {
    2. return number + 1
    3. }
    4. var myNumber = 1
    5. myNumber = oneMore(than: myNumber)
    6. print(myNumber)
    7. // Prints "2"

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

    重叠访问主要出现在代码中,该代码在结构的函数和方法或变异方法中使用输入/输出参数。在以下各节中讨论了使用长期访问的特定类型的Swift代码。

    对In-Out参数的访问冲突

    函数可以对其所有输入输出参数进行长期写访问。在对所有非输入参数进行评估之后,将开始对输入输出参数进行写访问,并持续该函数调用的整个过程。如果有多个输入/输出参数,则写入访问的开始顺序与参数出现的顺序相同。

    这种长期写访问的结果是,即使作用域规则和访问控制允许这样做,您也无法访问以in-out形式传递的原始变量-对原始文件的任何访问都会产生冲突。例如:

    1. var stepSize = 1
    2. func increment(_ number: inout Int) {
    3. number += stepSize
    4. }
    5. increment(&stepSize)
    6. // Error: conflicting accesses to stepSize

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

    ../_images/memory_increment_2x.png

    解决此冲突的一种方法是显式复制以下内容stepSize

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

    stepSize在调用之前制作的副本时increment(_:),很明显,的值copyOfStepSize会增加当前步长。读访问在写访问开始之前结束,因此没有冲突。

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

    1. func balance(_ x: inout Int, _ y: inout Int) {
    2. let sum = x + y
    3. x = sum / 2
    4. y = sum - x
    5. }
    6. var playerOneScore = 42
    7. var playerTwoScore = 30
    8. balance(&playerOneScore, &playerTwoScore) // OK
    9. balance(&playerOneScore, &playerOneScore)
    10. // Error: conflicting accesses to playerOneScore

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

    注意

    由于运算符是函数,因此他们也可以长期访问其输入输出参数。例如,如果balance(_:_:)是一个名为的运算符<^>,则写入将导致与相同的冲突playerOneScore <^> playerOneScorebalance(&playerOneScore, &playerOneScore)

    方法中的自我获取冲突

    self在方法调用期间,结构上的变异方法具有写权限例如,考虑一个游戏,其中每个玩家的健康值在受到伤害时都会减少,能量值在使用特殊能力时会减少。

    1. struct Player {
    2. var name: String
    3. var health: Int
    4. var energy: Int
    5. static let maxHealth = 10
    6. mutating func restoreHealth() {
    7. health = Player.maxHealth
    8. }
    9. }

    在上述restoreHealth()方法中,方法的写访问self从该方法的开头开始,一直持续到该方法返回为止。在这种情况下,内部没有其他代码restoreHealth()可以重叠访问Player实例的属性shareHealth(with:)下面方法将另一个Player实例作为输入输出参数,从而产生了访问重叠的可能性。

    1. extension Player {
    2. mutating func shareHealth(with teammate: inout Player) {
    3. balance(&teammate.health, &health)
    4. }
    5. }
    6. var oscar = Player(name: "Oscar", health: 10, energy: 10)
    7. var maria = Player(name: "Maria", health: 5, energy: 10)
    8. oscar.shareHealth(with: &maria) // OK

    在上面的示例中,调用shareHealth(with:)Oscar的播放器与Maria的播放器共享健康方法不会引起冲突。oscar在方法调用过程中可以进行写访问,因为它oscarselfmutating方法中的值,而maria在相同的持续时间内可以进行写访问,因为它maria是作为in-out参数传递的。如下图所示,它们访问内存中的不同位置。即使两个写访问在时间上重叠,它们也不会冲突。

    ../_images/memory_share_health_maria_2x.png

    但是,如果将oscar用作参数,则会shareHealth(with:)发生冲突:

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

    mutation方法需要self在该方法的持续时间内进行写访问,而in-out参数需要teammate在相同的持续时间内进行写访问在该方法中,两个self和都teammate指向内存中的同一位置-如下图所示。这两个写访问引用相同的内存,并且它们重叠,从而产生冲突。

    ../_images/memory_share_health_oscar_2x.png

    访问属性冲突

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

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

    在上面的示例中,调用balance(_:_:)元组的元素会产生冲突,因为对的写入访问重叠playerInformation双方playerInformation.healthplayerInformation.energy在输出参数,该装置被传递balance(_:_:)需要写访问他们的函数调用的持续时间。在这两种情况下,对元组元素的写访问都需要对整个元组的写访问。这意味着有两个写入访问,playerInformation其持续时间重叠,从而导致冲突。

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

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

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

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

    在上面的示例中,奥斯卡的健康和精力作为两个输入输出参数传递给balance(_:_:)编译器可以证明保留了内存安全性,因为这两个存储的属性不会以任何方式交互。

    保留内存安全性并非始终需要限制对结构属性的重叠访问。内存安全是理想的保证,但是独占访问是比内存安全更严格的要求-这意味着即使某些代码违反了对内存的独占访问,某些代码仍可以保留内存安全。如果编译器可以证明对内存的非独占访问仍然是安全的,则Swift允许使用此内存安全代码。具体来说,如果满足以下条件,则可以证明重叠访问结构的属性是安全的:

    • 您仅访问实例的存储属性,而不访问计算的属性或类属性。
    • 结构是局部变量的值,而不是全局变量的值。
    • 该结构要么没有被任何闭包捕获,要么仅被不冒号的闭包捕获。

    如果编译器无法证明访问是安全的,则它不允许访问。

  • 相关阅读:
    党务
    平台 大赛 公司
    音乐
    有趣的博主
    C++获取命令行参数命令
    360读全景
    3 海康网络相机官方例程(3)OpenCv + ffmpeg + rtmp 实现摄像头采集数据直播功能(不带cuda加速)
    使用opencv4进行分类器训练
    经典环境(2)OpenCV412+OpenCV-Contrib +vs2015+cuda10.1编译
    经典环境(1)OpenCV3.4.9+OpenCV-Contrib +vs2015+cuda10.1编译
  • 原文地址:https://www.cnblogs.com/strengthen/p/9739940.html
Copyright © 2011-2022 走看看