Is copy-on-write thread-safe?

So my understanding is that when you pass Swift structs around - Data, Array and so on - Swift won't do a full copy but instead use references, and will only allocate a real copy when you try and write through one of these references.


So I'm wondering - given that Swift's collection types are generally not thread-safe, is this copy-on-write behaviour thread-safe?


If I make 10 copies of an array, and then have 10 threads each start writing to their own copy, will Swift handle this gracefully? From a programming-model standpoint, the threads are each working on their own, independent piece of data, but under the covers there are 10 threads all trying to fork the same object which makes me a little unsure.

Replies

That question a bit old but anyway. What I found (by extensive unit testing) is that it is NOT thread safe. I preriodically see "index of range" error some where inside Array's "description" method on one thread while another thread modifies (append) the array. The Thread Sanitizer also catch a race condition quite well. Observing apple/swift source code repository I see that when it comes to CopyOnWrite it is made inside swift without any kind of locks.


BTW, to overcome this problem I started to make full copy of array under "read" lock, so that clients never see original mutable collection.

Like following:


class MyClass {

var items: [Int] { return readLock { return self.mutableItems.map { $0 } } }

func append(new: Int) { writeLock { self.mutableItems.append(new) } }

private var mutableItems = [Int]()

}


I know it's not ideal (constant copying of whole array), but at least multithreading clients of myClass can safely enumerate on "myclass.items" while others may do modifications.


And here is problematic code version:


class MyProblematicClass {

var items: [Int] { return self.mutableItems } // it doesn't matter whether you use "read" lock here or not

...

}

What I found (…) is that it is NOT thread safe.

Correct. I’m sorry you had to find this out the hard way )-: By way of explanation, check out the documentation for

isKnownUniquelyReferenced(_:) reference
, which shows a typical COW method behaving like this:
mutating func update(withValue value: T) {
    if !isKnownUniquelyReferenced(&myStorage) {
        myStorage = self.copiedStorage()
    }
    myStorage.update(withValue: value)
}

The race condition here is pretty clear.

Share and Enjoy

Quinn “The Eskimo!”
Apple Developer Relations, Developer Technical Support, Core OS/Hardware

let myEmail = "eskimo" + "1" + "@apple.com"

I have to diagree.


In Swift, Array, String, and Dictionary are all value types. They behave much like a simple int value in C, acting as a unique instance of that data. You don’t need to do anything special — such as making an explicit copy — to prevent other code from modifying that data behind your back. Importantly, you can safely pass copies of values across threads without synchronization. In the spirit of improving safety, this model will help you write more predictable code in Swift.

- Source: https://developer.apple.com/swift/blog/?id=10


The following code is thread-safe:


import Foundation

class SomeClass {
    let lock = NSLock()
    var numbers = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10]

    func getNumbers ( ) -> [Int] {
        lock.lock()
        let result = self.numbers
        lock.unlock()
        return result
    }

    func modifyNumbers ( ) {
        lock.lock()
        let first = self.numbers.removeFirst()
        self.numbers.append(first)
        lock.unlock()
    }
}

let numberContainer = SomeClass()

let thread1 = Thread() {
    while true {
        var numbers = numberContainer.getNumbers()
        // Numbers is now copy on write!
        // Modifying it will copy it.
        let first = numbers.removeFirst()
        numbers.append(first)
    }
}
thread1.start()

let thread2 = Thread() {
    while true {
        var numbers = numberContainer.getNumbers()
        // Numbers is now copy on write!
        // Modifying it will copy it.
        let first = numbers.removeFirst()
        numbers.append(first)
    }
}
thread2.start()

while true {
    numberContainer.modifyNumbers()
}


Three threads are constantly modifying a copy-on-write array and this causes no issues at all. Try it. Copy the code to test.swift:


swiftc -sanitize=thread test.swift && ./test


If this wasn't thread-safe, then Swift would not treat arrays as values types and they would not "behave much like a simple int value in C" since for int values in C the code above would also be thread-safe.


And in case you may think "Okay, then they must have changed that recently, this was definitely not the case when I wrote my reply", please note that the first blog post I linked is from Aug 15, 2014 and your reply is from Apr 14, 2017

Sorry, but this is incorrect. The code found at the documentation of

isKnownUniquelyReferenced(_:)
is not how copy-on-write is implemented for Swift standard types. And how to do it corretly to become thread-safe is even hinted below the code sample:


If the instance passed as

object
is being accessed by multiple threads simultaneously, this function may still return
true
. Therefore, you must only call this function from mutating methods with appropriate thread synchronization.


And that's how Swift standard types have implemented their copy-on-write. As otherwise the promise that Array, String, and Dictionary are value types in Swift would not hold true. Value types are are always thread-safe. See last paragraph of https://developer.apple.com/swift/blog/?id=10

Copy-on-write is thread-safe for all Swift standard types, like Strings, Arrays, or Dictionaries (and also sets, even not mentioned in the blog post linked below), as these are considered value types in Swift: https://developer.apple.com/swift/blog/?id=10


And value types are thread-safe by definition, so when you implement them as copy-on-write, this copy must be thread-safe, too, anything else would break the promisse that they are value types.


If you imlepment your own copy-on-write type, it's in your responsebility to ensure that the copy is thread safe:


If the instance passed as

object
is being accessed by multiple threads simultaneously, this function may still return
true
. Therefore, you must only call this function from mutating methods with appropriate thread synchronization.

- Source:

isKnownUniquelyReferenced(_:)