Skip to content

Instantly share code, notes, and snippets.

@drewmccormack
Created February 2, 2019 10:25
Show Gist options
  • Save drewmccormack/b1c4487935cf3c3e0a5feaf488a95ebd to your computer and use it in GitHub Desktop.
Save drewmccormack/b1c4487935cf3c3e0a5feaf488a95ebd to your computer and use it in GitHub Desktop.
Demonstrates how a Swift value constant can mutate when using Copy-on-Write (CoW) and multi-threading.
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
@ogres
Copy link

ogres commented Feb 4, 2019

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)

@dourgulf
Copy link

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)

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment