提问者:小点点

苹果对多线程引用和值类型的描述


我正在读苹果的留档。我以为我知道什么时候选择值类型,什么时候选择引用类型,但我又回到了Swif101。留档说:

  • 值类型:数据将在跨多个线程的代码中使用。
  • 引用类型:您想创建共享的可变状态

引用类型不也是跨多个线程共享的吗?这两行有什么区别?


共3个答案

匿名用户

正如其他人指出的那样,引用类型总是传递一个指向对象的指针,这是您想要“共享的可变状态”的理想选择(正如您引用的文档所说)。不过,很明显,如果您正在跨多个线程改变/访问引用类型,请确保同步您对它的访问(通过专用的串行队列、读取器-写入器模式、锁等)。

不过,值类型有点复杂。是的,正如其他人指出的那样,如果您将值类型作为参数传递给一个方法,然后该方法在另一个线程上执行某些操作,那么您实际上是在使用该值类型的副本(尽管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类型的标准写时复制例外:仅当数据发生突变时才执行实际复制。

匿名用户

措辞混乱。

值类型:数据将在跨多个线程的代码中使用。

通过这一点,我相信他们的意思是,当您希望许多线程从您的数据中读取数据时,它很有用。这是因为您知道,无论何时为新线程提供数据的副本,您都不会使任何其他副本面临来自其他线程的意外突变的风险。

在这样的上下文中使用值类型,您不需要共享可变状态,可以避免处理引用类型时产生的许多类错误(竞争条件、死锁、活锁等)。

引用类型:您想创建共享的可变状态

只有引用类型可以在线程之间共享,方法是让两个线程各自保留对共享实例的引用。