【问题标题】:Array pass by value by default & thread-safety数组默认传值 & 线程安全
【发布时间】:2018-01-08 06:59:36
【问题描述】:

假设我有一个类有 Array 对象 Photo

class PhotoManager {
  fileprivate var _photos: [Photo] = []

  var photos: [Photo] {
      return _photos
  }
}

我读到了one article,上面写着:

默认情况下,Swift 中的类实例是通过引用传递的,并且 按值传递的结构。 Swift 的内置数据类型,如 Array 和 字典,被实现为结构。

意思是上面的getter返回了[Photo]数组的一个副本。

然后,同一篇文章试图通过将代码重构为使 getter 线程安全:

fileprivate let concurrentPhotoQueue = DispatchQueue(label: "com.raywenderlich.GooglyPuff.photoQueue",
                                                       attributes: .concurrent)
  fileprivate var _photos: [Photo] = []
  var photos: [Photo] {
    var photosCopy: [Photo]!
    concurrentPhotoQueue.sync {
        photosCopy = self._photos
    }
    return photosCopy
  }

上面的代码明确地在getter中复制了self._photos

我的问题是:

  1. 如果默认情况下 swift 已经像文章开头所说的那样返回一个副本(按值传递),为什么要将文章再次复制到photosCopy 以使其成为线程安全的?我觉得我自己并不完全理解那篇文章中提到的这两部分。

  2. Swift3 真的像文章所说的那样默认为Array 实例传递值吗?

有人可以帮我澄清一下吗?谢谢!

【问题讨论】:

    标签: swift multithreading swift3 concurrency thread-safety


    【解决方案1】:

    我会反过来回答你的问题:

    1. Swift3 真的像文章说的那样默认为 Array 实例传值吗?

    简单的答案:是的

    但我猜这不是您在询问“Swift3 真的是否按值传递”时关心的问题。 Swift 的行为 就好像 数组被整个复制,但在幕后它优化了操作并且整个数组不会被复制,直到,如果,它需要。 Swift 使用一种称为写时复制 (COW) 的优化。

    然而对于 Swift 程序员来说,如何复制并不像操作的语义那么重要——也就是说,在赋值/复制之后,这两个数组是独立和变异的一个不会影响另一个。

    1. 如果默认情况下 swift 已经像文章开头所说的那样返回了一个副本(按值传递),为什么要将文章再次复制到 photosCopy 以使其成为线程安全的?我觉得我自己并没有完全理解那篇文章中提到的这两部分。

    这段代码所做的是确保复制以线程安全的方式完成。

    数组不是一个微不足道的值,它被实现为多字段结构,其中一些字段引用其他结构和/或对象 - 这是支持数组大小增长等能力所必需的。

    在多线程系统中,一个线程可能会尝试复制数组,而另一个线程会尝试更改数组。如果允许这些同时发生,那么事情很容易出错,例如在复制过程中,数组可能会发生变化,从而导致无效的复制 - 部分是旧值,部分是新值。

    Swift 本身 不是线程安全的;特别是它不会阻止在执行复制时更改数组。您拥有的代码通过使用 GCD 队列来解决此问题,以便在一个线程对数组进行任何更改期间,任何其他线程中对数组的所有其他写入或读取都将被阻止,直到更改完成。

    您可能还担心它们在此处存在多个副本,self._photosphotoCopy,然后是 photoCopy 到返回值。虽然语义这是在实践中发生的情况,但可能只有一个复杂的副本(这将是线程安全的),因为 Swift 系统将进行优化。

    HTH

    【讨论】:

    • 感谢您的解释。我想我现在明白了! :)
    • 虽然我接受了另一个答案,但我建议人们也阅读@CRD的答案,这也很有帮助。
    【解决方案2】:

    1) 在代码示例中,您提供的内容将返回_photos 的副本。 正如文章中所写:

    The getter for this property is termed a read method as it’s reading
    the mutable array. The caller gets a copy of the array and is protected 
    against mutating the original array inappropriately.
    

    这意味着您可以从课堂外访问_photos,但您不能从那里更改它们。 photos 的值只能在类内更改,从而保护该数组免受意外更改。

    2)是的,Array 是一个值类型的结构体,它将按值传递。您可以在 Playground 中轻松查看

    let arrayA = [1, 2, 3]
    var arrayB = arrayA
    
    arrayB[1] = 4 //change second value of arrayB
    
    print(arrayA) //but arrayA didn't change
    

    UPD #1

    在文章中,他们有方法 func addPhoto(_ photo: Photo) 将新照片添加到 _photos 数组是什么使对该属性的访问不是线程安全的。这意味着_photos 的值可以同时在几个线程上更改,这会导致问题。

    他们通过在concurrentQueue.barrier 上写入照片来修复它,这使它成为线程安全的,_photos 数组将每次更改一次

    func addPhoto(_ photo: Photo) {
      concurrentPhotoQueue.async(flags: .barrier) { // 1
        self._photos.append(photo) // 2
        DispatchQueue.main.async { // 3
          self.postContentAddedNotification()
        }
      }
    } 
    

    现在为了确保线程安全,您需要读取同一队列上的 _photos 数组。这就是他们重构 read 方法的唯一原因

    【讨论】:

    • 我已经发布了两个代码 sn-ps,我要问的是第一个代码 sn-p,文章说 read 方法返回一个可变数组的副本,我的困惑是如果是这样的话为什么文章的作者将代码重构为我发布的第二个代码sn-p,如果默认情况下已经返回数组的副本,则显式复制数组以返回?
    • 感谢您的更新,但它并没有清楚地回答我的问题。你的更新提到的是我已经理解的。我的问题只是关于“复制”部分。首先,作者在swift中说默认情况下,array是一个传值的结构体,也就是说默认情况下,read方法已经返回了数组的一个副本。那么,如果默认情况下它是快速复制的,为什么稍后它会再次显式复制数组?在我看来,代码复制了两次数组。
    • @Leem.fin 他们之所以做到这一点,不是因为它制造了array copy,而是为了保护_photos 阵列收集所有照片(不是photos 财产)免于课外访问。所以你不能只通过addPhoto(_ photo: Photo)方法直接写到_photos属性
    • 那么“数组默认快速返回副本”的说法仅适用于photos属性阅读器?
    • @Leem.fin 复制一些集合意味着一项复杂的任务,可以“原子地”完成以保证线程安全。如果您只想简单地“返回副本”,则必须确保在创建副本时未修改原始集合的某些部分。出于这个原因,他们在示例中以同步方式制作副本。他们以正确的方式做到了:-)。我建议你接受这个答案作为一个有效的答案。
    猜你喜欢
    • 2016-11-28
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2023-03-12
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多