我正在读苹果的留档。我以为我知道什么时候选择值类型,什么时候选择引用类型,但我又回到了Swif101。留档说:
引用类型不也是跨多个线程共享的吗?这两行有什么区别?
正如其他人指出的那样,引用类型总是传递一个指向对象的指针,这是您想要“共享的可变状态”的理想选择(正如您引用的文档所说)。不过,很明显,如果您正在跨多个线程改变/访问引用类型,请确保同步您对它的访问(通过专用的串行队列、读取器-写入器模式、锁等)。
不过,值类型有点复杂。是的,正如其他人指出的那样,如果您将值类型作为参数传递给一个方法,然后该方法在另一个线程上执行某些操作,那么您实际上是在使用该值类型的副本(尽管Josh关于写时复制的说明)。这确保了传递给方法的对象的完整性。这很好(并且已经被这里的其他答案充分覆盖)。
但是当您处理闭包时,它会变得更加复杂。例如,考虑以下内容:
struct Person {
var firstName: String
var lastName: String
}
var person = Person(firstName: "Rob", lastName: "Ryan")
DispatchQueue.global().async {
Thread.sleep(forTimeInterval: 1)
print("1: \(person)")
}
person.firstName = "Rachel"
Thread.sleep(forTimeInterval: 2)
person.lastName = "Moore"
print("2: \(person)")
显然,您通常不会睡眠
,但我这样做是为了说明这一点:也就是说,即使我们正在处理值类型和多个线程,您在闭包中引用的人员
与您在主线程(或运行它的任何线程)上处理的实例相同,而不是它的副本。如果您正在处理一个可变对象,那不是线程安全的。
我设计了这个例子来说明这一点,其中上面闭包中的print
语句将报告“Rachel Ryan”,有效地显示Person
值类型处于不一致状态的状态。
使用值类型的闭包,如果您想享受值语义学,您必须更改async
调用以使用单独的变量:
let separatePerson = person
queue.async {
Thread.sleep(forTimeInterval: 1)
print("1: \(separatePerson)")
}
或者,更容易的是,使用“捕获列表”,它指示闭包应该捕获哪些值类型变量:
queue.async { [person] in
Thread.sleep(forTimeInterval: 1)
print("1: \(person)")
}
通过这些示例中的任何一个,您现在都在享受值语义学,复制对象,并且print
语句将正确报告“Rob Ryan”,即使原始人
对象在另一个线程上发生了突变。
因此,如果您正在处理值类型和闭包,值类型可以跨线程共享,除非您显式使用捕获列表(或等效的东西)以享受值语义学(即根据需要复制对象)。
是的,如果多个线程使用它们,引用是共享的;这正是问题所在。所有线程都引用内存中相同的实际数据。然后它们需要同步机制来确保单独线程的读写不会冲突。这些机制在代码复杂性和性能方面都有成本。
值类型的实例不共享:每个线程都有自己的副本。*这意味着每个线程都可以读取和写入其实例,而不必担心其他线程在做什么。
*对于Swift stdlib类型的标准写时复制例外:仅当数据发生突变时才执行实际复制。
措辞混乱。
值类型:数据将在跨多个线程的代码中使用。
通过这一点,我相信他们的意思是,当您希望许多线程从您的数据中读取数据时,它很有用。这是因为您知道,无论何时为新线程提供数据的副本,您都不会使任何其他副本面临来自其他线程的意外突变的风险。
在这样的上下文中使用值类型,您不需要共享可变状态,可以避免处理引用类型时产生的许多类错误(竞争条件、死锁、活锁等)。
引用类型:您想创建共享的可变状态
只有引用类型可以在线程之间共享,方法是让两个线程各自保留对共享实例的引用。