根本问题在于对“每个线程都有自己的副本”的解释。
是的,我们通常使用值类型来确保线程安全,为每个线程提供对象(如数组)的自己的副本。但这并不意味着值类型可以保证每个线程都会获得自己的副本。
具体而言,使用闭包,多个线程可以尝试改变同一个值类型对象。以下是一段代码示例,展示了一些与 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提供了简单的机制(其中一些完全自动和透明),可以为每个线程提供自己的副本,从而更轻松地编写线程安全的代码。
以下是另一个使用另一个值类型的示例,使问题更加明显。以下是一个未能编写线程安全代码返回语义无效对象的示例:
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
调用中同步或者也使用值语义是必须的。)
由于Array
是值类型,您可以确保它只有一个直接的所有者。
问题出现在当数组有多个间接的所有者时。考虑以下示例:
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
现在正在运行线程2
array.isEmpty
计算结果为false,保护通过,并且执行将继续通过它
执行array += [Int](1...10)
。现在,array
等于[1, 2, 3, 4, 5, 6, 7, 8, 9]
线程2完成,放弃CPU,操作系统调度线程1
线程1恢复到上次离开的位置。
执行array += [Int](1...10)
。现在,array
等于[1, 2, 3, 4, 5, 6, 7, 8, 9, 1, 2, 3, 4, 5, 6, 7, 8, 9]
。这本不该发生的!
[1, 2, 3, 4, 5, 6, 7, 8, 9, 1, 2, 3, 4, 5, 6, 7, 8, 9]
这样混乱,甚至更加混乱。|| 非常感谢您的答案Alex。如果我可以标记两个答案,我会这么做的。 - mfaaniclass 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
——而没有复制它,因此如果同时调用myRead和myWrite函数,则可能会发生竞态条件。您必须通过使用队列来解决(请参见此处)该问题。