-
-
Save drewmccormack/b1c4487935cf3c3e0a5feaf488a95ebd to your computer and use it in GitHub Desktop.
import Foundation | |
struct NuclearPowerStationOperator { | |
class Storage { | |
var turnOffCores: Bool = false | |
func copy() -> Storage { | |
let new = Storage() | |
new.turnOffCores = turnOffCores | |
return new | |
} | |
} | |
private var storage: Storage = Storage() | |
var turnOffCores: Bool { | |
get { | |
return storage.turnOffCores | |
} | |
set { | |
if isKnownUniquelyReferenced(&storage) { | |
Thread.sleep(forTimeInterval: 1.0) // Sleep to simulate race condition | |
storage.turnOffCores = newValue | |
} else { | |
storage = storage.copy() | |
storage.turnOffCores = newValue | |
} | |
} | |
} | |
var description: String { | |
return "\(turnOffCores ? "We are in danger" : "We are safe")" | |
} | |
} | |
// Create a mutable value | |
var crazyOperator = NuclearPowerStationOperator() | |
DispatchQueue.global(qos: .background).async { | |
Thread.sleep(forTimeInterval: 0.5) // Sleep a little to give main thread time to start setting property | |
let saneOperator = crazyOperator // Create a constant copy of the operator from main thread | |
print(saneOperator.description) // Print our intial property value | |
Thread.sleep(forTimeInterval: 2.0) // Simulate race by waiting for setter on main thread to finish | |
print(saneOperator.description) // Test property (it will be different) | |
} | |
// Update the value. Note that the setter simulates a race condition by being very slow | |
crazyOperator.turnOffCores = true |
@ravikandhadai A context switch is not relevant. The relevant part is that if you share a reference to a value type, and concurrently read/write to that reference from multiple threads, your program is buggy. That's the end of the discussion.
Specifically, the behaviour here has racing reads and writes from multiple threads without synchronisation. As @helje5 points out, that's not safe to do with any value, ever.
You can see this clearly by translating @drewmccormack's simplified example on Int
:
var i: Int = 0
DispatchQueue.global(qos: .background).async {
let o = i
print("\(o) original value copy", o)
usleep(10)
print("\(o) final value of copy", o)
}
i += 1
into C:
int i = 0;
void test(void) {
dispatch_async(dispatch_get_global_queue(QOS_CLASS_BACKGROUND, 0), ^{
int o = i;
printf("Here's the value: %d\n", o);
});
i = 1;
}
test();
This code is clearly not right. There is a race on the read from i
and the write to i
. On some architectures, this may happen to work some of the time, but it is fundamentally not thread safe. You are never allowed to race unsynchronised reads and writes from multiple threads. The fact that CoW values make this harder does not change the fact that it is simply never safe to do this.
You have to add crazyOperator
to a capture list of the closure to get the behaviour you wanted. Without it, closure will have a reference to your variable, it will not copy it on creation.
Change
DispatchQueue.global(qos: .background).async {
into
DispatchQueue.global(qos: .background).async { [crazyOperator] in
To see the difference between a closure with and without a capture list, have a look at the following code:
import Foundation
var int: Int? = 100
var array: [Int]? = [200]
DispatchQueue.global(qos: .background).async {
let copy_int = int
let copy_array = array
print("Without capture list int: \(String(describing: copy_int)), array: \(String(describing: copy_array))")
}
DispatchQueue.global(qos: .background).async { [int, array] in
let copy_int = int
let copy_array = array
print("With capture list int: \(String(describing: copy_int)), array: \(String(describing: copy_array))")
}
int = nil
array = nil
Thread.sleep(forTimeInterval: 1)
You have to add
crazyOperator
to a capture list of the closure to get the behaviour you wanted. Without it, closure will have a reference to your variable, it will not copy it on creation.Change
DispatchQueue.global(qos: .background).async {into
DispatchQueue.global(qos: .background).async { [crazyOperator] inTo see the difference between a closure with and without a capture list, have a look at the following code:
import Foundation var int: Int? = 100 var array: [Int]? = [200] DispatchQueue.global(qos: .background).async { let copy_int = int let copy_array = array print("Without capture list int: \(String(describing: copy_int)), array: \(String(describing: copy_array))") } DispatchQueue.global(qos: .background).async { [int, array] in let copy_int = int let copy_array = array print("With capture list int: \(String(describing: copy_int)), array: \(String(describing: copy_array))") } int = nil array = nil Thread.sleep(forTimeInterval: 1)
In this demo, you're not actually modifying the variable in multiple threads, the copy of the variable is on the same thread, which is completely different from the topic under discussion.
Nice illustration of an anti-pattern for implementing COW types. At a high level, it seems the best fix would be to ensure that the check
isUniquelyReferenced(&storage)
and the updation:storage.turnOffCores = newValue
happens atomically. That is, no context switch should be possible between the two.