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 |
This comment has been minimized.
This comment has been minimized.
To adjust your example, this is one way to protect the concurrent write to shared state: let lock = NSLock()
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
lock.lock()
let saneOperator = crazyOperator // Create a constant copy of the operator from main thread
lock.unlock()
// Note that the saneOperator is now thread-safe!
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
lock.lock()
crazyOperator.turnOffCores = true
lock.unlock() |
This comment has been minimized.
This comment has been minimized.
I'm afraid I disagree. The CoW example is not equivalent to a true value type, like an 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
The two print statements will always print the same value here. Whether that is the value you want is a different question, but once In my CoW example, |
This comment has been minimized.
This comment has been minimized.
Yes, how they fail is different because the implementation is different. Yet both versions are incorrect and lead to junk. If your point was to demonstrate that CoW structures being used incorrectly lead to different junk than Int when being used incorrectly, that is fair! But I'm not sure how helpful that is :-) The value-type in Swift promise for both Int or a CoW based value type is:
Unlocked writing to shared state may yield to different programs failures, but both are equally buggy and undefined. And in fact the same is true for many more variants of value types, not just CoW types (like a |
This comment has been minimized.
This comment has been minimized.
A similar implementation in Rust would be the following: use std::thread;
use std::time::Duration;
struct NuclearPowerStationOperator {
turn_off_core: bool,
}
fn main() {
let mut crazy_operator = NuclearPowerStationOperator {
turn_off_core: false,
};
let handle = thread::spawn(move || {
thread::sleep(Duration::from_millis(500));
let sane_operator = crazy_operator;
println!("Core is {}", sane_operator.turn_off_core);
thread::sleep(Duration::from_millis(2000));
println!("Core is {}", sane_operator.turn_off_core);
});
thread::sleep(Duration::from_millis(1000));
crazy_operator.turn_off_core = true;
handle.join().unwrap();
} If I run
Then if I run the executable with
The difference is with Rust, a data type needs to implement Regardless, the build warning shows you that the assignment there should not happen. |
This comment has been minimized.
This comment has been minimized.
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 |
This comment has been minimized.
This comment has been minimized.
@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 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 |
This comment has been minimized.
This comment has been minimized.
You have to add 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) |
This comment has been minimized.
Again to quote @jckarter: https://twitter.com/jckarter/status/975729078354395137
What you are demonstrating here is this, just in complicated:
While that works most of the time, it is incorrect. You either need to do:
or use another synchronisation mechanism, like AtomicIncrement. All value types work just like Int here, regardless whether they use CoW or not.