Swift - 设置变量属性并发或多线程安全吗?

tru*_*duc 5 ios swift

据我所知,在使用并发或多线程时设置变量属性并不安全,但我不能使用下面的代码产生崩溃。

\n\n
class Node {\n    var data = 0\n}\n\nvar node = Node()\nlet concurrentQueue = DispatchQueue(label: "queue", attributes: .concurrent)\n\nfor i in 0...1000 {\n    concurrentQueue.async {\n        node.data = i    // Should get crash at this line\n    }\n}\n
Run Code Online (Sandbox Code Playgroud)\n\n

更新1

\n\n

感谢@MartinR 在评论中指出。

\n\n
\n

启用\xe2\x80\x9cThread Sanitizer\xe2\x80\x9d,它会立即报告错误。

\n
\n\n

更新2

\n\n

EXC_BAD_ACCESS KERN_INVALID_ADDRESS如果更改data为引用类型,代码会崩溃。它并不总是发生,但有时会发生。例如:

\n\n
class Data {}\n\nclass Node {\n    var data = Data()    // Use reference type instead of value type\n}\n\nvar node = Node()\nlet concurrentQueue = DispatchQueue(label: "queue", attributes: .concurrent)\n\nfor i in 0...1000 {\n    concurrentQueue.async {\n        node.data = Data()    // EXC_BAD_ACCESS KERN_INVALID_ADDRESS\n    }\n}\n
Run Code Online (Sandbox Code Playgroud)\n\n

这种行为在 Objective-C 中也会发生。同时设置对象属性会导致崩溃。但对于原始类型,崩溃就不会发生。

\n\n

问题

\n\n
    \n
  • 同时设置值类型属性会导致崩溃吗?
  • \n
  • 如果它不会产生崩溃,那么设置值类型属性和设置引用类型属性有什么区别?
  • \n
\n\n

如果有人也能解释为什么同时设置引用类型属性会产生崩溃,那就完美了。

\n

Duc*_*yen 3

首先

类值存储在堆内存中,而结构/枚举值存储在堆栈内存中,编译器将尝试在编译时分配这些内存(根据我的第一个参考和许多在线答案)。您可以使用此代码进行检查:

class MemTest {}
class Node {
    var data = MemTest()
}
let node = Node()
let concurrentQueue = DispatchQueue(label: "queue", attributes: .concurrent)

for index in 0...100000 {
concurrentQueue.async {
    node.data = MemTest()
    withUnsafePointer(to: &node.data) {
        print("Node data @ \($0)")
    }
    withUnsafePointer(to: node.data) {
        print("Node data value no. \(index) @ \($0)")
    }
}
Run Code Online (Sandbox Code Playgroud)

如何:运行 2 次并检查内存地址是否有值更改时间 500,在类和结构之间切换 MemTest 将显示差异。结构每次都会显示相同的地址,而类会显示不同的地址。所以改变值类型就像改变地址一样,但是内存块不会被恢复,而改变引用类型不仅仅是改变地址,还会恢复并分配新的内存块,这会导致程序中断。

第二

但是如果使用 withUnsafePointer 运行 @trungduc 的第一个代码块,它会告诉我们 for 循环的索引 var 是在运行中分配的,并且在堆内存中,那么这是为什么呢?如前所述,如果值类型在编译时可计算,编译器只会尝试分配内存。如果它们不可计算,则该值将被分配在堆内存中并保留在那里直到范围结束(根据我的第二个参考)。所以这里的解释可能是系统将在一切完成后恢复分配的堆栈 - 范围结束(我不太确定)。因此,在这种情况下,代码不会产生我们所知的崩溃。我的结论是,在这种情况下,引用类型变量的内存将被无限制地分配和恢复,而值类型变量的内存只有在系统进入和退出包含该变量的范围后才会被分配和删除

谢谢

一些单词

我的回答不是很扎实,可能有很多语法和拼写错误。所有更新均表示赞赏。提前致谢

更新

对于这部分我不太确定:变量index被使用运算符复制到新的内存地址=,因此范围结束在哪里并不重要,堆栈将在for循环后释放经过一些挖掘,在@trungduc的代码中,使用引用类型变量,它会做三件事:

  1. 为类分配新内存Data
  2. 减少对Data存储在 中的old 的引用,甚至在不再引用时node.data释放 oldData
  3. 指向node.data新的Data

而对于值类型它只会做一件事:

  1. 指向堆栈内存node.data中Integer的主要区别在于步骤 2,其中有可能Data恢复旧内存对于引用类型,有可能会发生这种情况
________Task 1________|________Task 2________
Allocate new Data #1  |
                      |Allocate new Data #2
Load pointer to old   |
Data                  |
Reduce reference count|
to old Data           |
                      |Load pointer to old
                      |Data
Free old Data         |
                      |Reduce reference count
                      |to old Data (!)
                      |Free old Data (!)
Reference new Data #1 |
                      |Reference new Data #2
Run Code Online (Sandbox Code Playgroud)

而对于值类型,则会发生这种情况

________Task 1________|________Task 2________
Reference to Integer 1|
                      |Reference to Integer 2
Run Code Online (Sandbox Code Playgroud)

在第一种情况下,我们将有各种替代方案,但在大多数情况下,我们会遇到分段错误,因为线程 2 在线程 1 释放该指针后尝试取消引用该指针。正如我们所注意到的,可能还有其他问题,例如内存泄漏,线程 2 可能无法正确减少对数据 #1 的引用计数,而在第二种情况下,它只是更改指针。

笔记

第二种情况,在Intel CPU上永远不会导致崩溃,但在其他CPU上也不能保证,因为许多CPU不承诺这样做不会导致崩溃。