如果数组是值类型并因此被复制,那么它们为什么不是线程安全的?

If arrays are value types and therefore get copied, then how are they not thread safe?

阅读我了解到:

Instances of value types are not shared: every thread gets its own copy.* That means that every thread can read and write to its instance without having to worry about what other threads are doing.

然后我被带到 this 答案及其评论

并被告知:

an array, which is not, itself, thread-safe, is being accessed from multiple threads, so all interactions must be synchronized.

& 关于 每个线程都有自己的副本 有人告诉我

if one thread is updating an array (presumably so you can see that edit from another queue), that simply doesn't apply

根本不适用 <-- 为什么不呢?

我最初认为所有这一切都是因为数组,即值类型被包装到 class 但令我惊讶的是我被告知不是真的!所以我又回到了 Swift 101 :D

根本问题是"every thread gets its own copy"的解释。

是的,我们经常使用值类型来确保线程安全,方法是为每个线程提供其自己的对象(例如数组)副本。但这与声称值类型保证每个线程都将获得自己的副本不是一回事。

具体来说,使用闭包,多个线程可以尝试改变相同的值类型对象。下面是一个代码示例,显示了一些与 Swift Array 值类型交互的非线程安全代码:

let queue = DispatchQueue.global()

var employees = ["Bill", "Bob", "Joe"]

queue.async {
    let count = employees.count
    for index in 0 ..< count {
        print("\(employees[index])")
        Thread.sleep(forTimeInterval: 1)
    }
}

queue.async { 
    Thread.sleep(forTimeInterval: 0.5)
    employees.remove(at: 0)
}

(您通常不会添加 sleep 调用;我只是将它们添加到显性竞争条件中,否则很难重现。您也不应该像这样在没有同步的情况下从多个线程中改变一个对象,但我这样做是为了说明问题。)

在这些 async 调用中,您仍然指的是之前定义的同一个 employees 数组。所以,在这个特定的例子中,我们会看到它输出 "Bill",它会跳过 "Bob"(即使它是 "Bill" 被删除),它会输出 "Joe" (现在是第二项),然后尝试访问现在只剩下两项的数组中的第三项时会崩溃。

现在,我在上面说明的所有内容是单个值类型 可以 在被另一个线程使用时被一个线程改变,从而违反线程安全。在编写非线程安全的代码时,实际上会出现一系列更基本的问题,但以上只是一个稍微做作的例子。

但是,您可以通过向第一个 async 调用添加一个 "capture list" 来确保这个单独的线程获得它自己的 employees 数组副本,以表明您想要工作使用原始 employees 数组的副本:

queue.async { [employees] in
    ...
}

或者,如果您将此值类型作为参数传递给另一个方法,您将自动获得此行为:

doSomethingAsynchronous(with: employees) { result in
    ...
}

在这两种情况中的任何一种情况下,您都会享受值语义并看到原始数组的副本(或写时复制),尽管原始数组可能已在其他地方发生了变化。

最重要的是,我的观点只是值类型不保证每个线程都有自己的副本。 Array 类型不是(许多其他可变值类型也是)线程安全的。但是,与所有值类型一样,Swift 提供简单的机制(其中一些是完全自动和透明的),为每个线程提供自己的副本,从而更容易编写线程安全代码。


这是另一个值类型的示例,它使问题更加明显。这是一个无法编写线程安全代码 returns 语义无效对象的示例:

let queue = DispatchQueue.global()

struct Person {
    var firstName: String
    var lastName: String
}

var person = Person(firstName: "Rob", lastName: "Ryan")

queue.async {
    Thread.sleep(forTimeInterval: 0.5)
    print("1: \(person)")
}

queue.async { 
    person.firstName = "Rachel"
    Thread.sleep(forTimeInterval: 1)
    person.lastName = "Moore"
    print("2: \(person)")
}

在这个例子中,第一个打印语句实际上会说 "Rachel Ryan",它既不是 "Rob Ryan" 也不是 "Rachel Moore"。简而言之,我们正在检查处于内部不一致状态的 Person

但是,同样,我们可以使用捕获列表来享受值语义:

queue.async { [person] in
    Thread.sleep(forTimeInterval: 0.5)
    print("1: \(person)")
}

在这种情况下,它会说 "Rob Ryan",而没有注意到原来的 Person 可能正处于被另一个线程变异的过程中。 (显然,仅通过在第一个 async 调用中使用值语义并不能解决真正的问题,但也可以在此处使用值语义同步第二个 async 调用 and/or。)

因为 Array 是一个值类型,你可以保证它有一个 direct 所有者。

问题来自于数组有多个 indirect 所有者时发生的情况。考虑这个例子:

Class Foo {
    let array = [Int]()

    func fillIfArrayIsEmpty() {
        guard array.isEmpty else { return }
        array += [Int](1...10)
    }
}

let foo = Foo();

doSomethingOnThread1 {
    foo.fillIfArrayIsEmpty()
}

doSomethingOnThread2 {
    foo.fillIfArrayIsEmpty()
}

array 有一个直接所有者:它所在的 foo 实例。但是,线程 1 和线程 2 都拥有 foo 的所有权,并且传递地拥有 array 在其中。这意味着他们都可以异步地改变它,所以竞争条件可能会发生。

以下是可能发生的情况的示例:

  • 线程 1 启动 运行

  • array.isEmpty求值为false,守卫通过,通过它继续执行

  • 线程 1 已用完其 CPU 时间,因此它从 CPU 开始。线程 2 由 OS

  • 安排
  • 线程 2 现在是 运行

  • array.isEmpty求值为false,守卫通过,通过它继续执行

  • array += [Int](1...10) 被执行。 array 现在等于 [1, 2, 3, 4, 5, 6, 7, 8, 9]

  • 线程 2 完成,并放弃 CPU,线程 1 由 OS

  • 调度
  • 线程 1 从中断处恢复。

  • array += [Int](1...10) 被执行。 array 现在等于 [1, 2, 3, 4, 5, 6, 7, 8, 9, 1, 2, 3, 4, 5, 6, 7, 8, 9]。这不应该发生!

你的假设是错误的。你认为无论你对结构体做什么,副本都会神奇地发生。不对。如果你复制它们,它们将被简单地复制。

class someClass{ 
var anArray : Array = [1,2,3,4,5]

func copy{
var copiedArray = anArray // manipulating copiedArray & anArray at the same time will NEVER create a problem
} 

func myRead(_ index : Int){
print(anArray[index])
}

func myWrite(_ item : Int){
anArray.append(item)
}
}    

但是在您的读写函数中您正在访问anArray而不复制它,所以race-conditions 可以 如果同时调用 myRead 和 myWrite 函数,则会发生。您必须使用队列解决(参见 here)问题。