一尘不染

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

swift

值类型的实例不共享:每个线程都有自己的副本。*这意味着每个线程都可以读写其实例,而不必担心其他线程在做什么。

然后我被带到了这个答案及其评论

并被告知:

从多个线程访问的数组本身本身不是线程安全的,因此所有交互必须同步。

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

如果一个线程正在更新数组(大概是这样,您就可以从另一个队列中看到该编辑内容),那根本就不适用

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

我最初以为所有这些事情都是在发生,因为数组(即值类型)被包装到一个类中,但是令我惊讶的是,我被告知不是真的!所以我又回到了Swift 101:D


阅读 347

收藏
2020-07-07

共1个答案

一尘不染

根本问题是“每个线程都有自己的副本”的解释。

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

具体来说,使用闭包,多个线程可以尝试变异同一值类型的对象。这是一个代码示例,显示了一些与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”(现在是第二项),并且那么尝试访问数组中的第三个项目将崩溃,该数组现在只剩下两个项目。

现在,我在上面说明的所有事情是,一个值类型 可以
在一个线程中被另一个线程使用,而又被另一个线程使用,从而违反了线程安全性。实际上,在编写不是线程安全的代码时,可能会出现一系列更基本的问题,但是以上只是一个稍微作弊的示例。

但是,您可以employees通过在第一个async调用中添加一个“捕获列表”
来表示要使用原始employees数组的副本,从而确保该单独的线程获得其数组的副本:

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

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

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

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

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


这是另一个示例,其中另一个值类型使问题更加明显。这是一个示例,其中编写线程安全代码失败返回语义上无效的对象:

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调用和/或在那里使用值语义来解决。)

2020-07-07