如果数组是值类型并因此被复制,那么它们为什么不是线程安全的?
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)问题。
阅读
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)问题。