Why Swift’s Copy-on-Write Is Safe
I’ve been applying the Copy-on-Write pattern for structs in Swift for quite a while, but is it actually thread safe? Is there not a risk of a race condition between checking that the object is uniquely referenced, and returning the reference?
It’s thread safe to read and copy but not write (modulo bugs). It should be as thread safe as an int variable
The difference from an ObjC object would be that two threads can both copy from a common value and modify their local copy with a guarantee that the writes don’t race, so this is valid:
let x = [1, 2, 3] q.async { var y = x; y[0] = 4 } q.async { var y = x; y[0] = 4 }[…]
If you implement your cow buffer the same way the standard library does, using
isUniquelyReferenced
to check whether copying is necessary before any modification, then you should get the same guarantee. TheisUniquelyReferenced
is itself threadsafe
I wasn’t even particularly thinking of Apple’s types, but more just the way we are told to do it in our own. If I understand, it is possible for a CoW struct to change value unexpectedly without you doing anything. Seems like would be pretty serious violation of value semantics.
Retain, release, and isUnique are all atomic, and ARC ensures that the read will ensure an independent retain for each thread. There should be no “between”
I think Drew is not concerned with isUnique’s atomicity, but with the atomicity of the return/copy code that follows.
isUnique
takes its argumentinout
intentionally to ensure this isn't a problem. Swift'sinout
requires exclusive access to the memory passed in, so by the time you have a local copy, it must be in a separate memory location with its own strong referenceIn other words, because of the
inout
exclusivity guarantee,isUnique
returning true also implies that your thread is the only thread that can see the one outstanding reference
See also: ManagedBuffer.swift.
I guess they key thing to understand is that this is not thread safe and needs synchronization:
var x = [ 1, 2, 3 ] q.async( x.append(4) ) q.async( x.append(5) )
I’m afraid I am still not completely convinced. Contested writes are always a risk, but I can live with that. I am more worried that there could be violations of value semantics. Here is a sample which creates a let constant, which subsequently mutates.
makes sense. From this code (image), unless I miss sth, the second reference can be created (2nd thread) after the atomic test (1st thread) but before the update. This would mean that the copied value would be changed too.
I think part of the confusion is that people are not talking about the same thing. The way I think about it, Swift’s CoW protects variables that are declared as let
. Other code can take the value, put it in a var
, and mutate it, and the original variable will be unchanged, even if it’s not protected by a lock or queue. The reason this works is that a mutating
method can only be called if the value is already in a var
. By the time that happens, the reference count will be at least 2 (the original let
, plus the var
). ARC-itself is thread-safe. So, at the time of the mutation, isKnownUniquelyReferenced()
will return false, and it will have to make a copy.
In McCormack’s example, the original value is in a var
, and then it goes into a let
, and the object inside the struct changes after the struct has been copied in the let
. This is unfortunate, but—as with primitive types—you aren’t supposed to be writing without synchronization. It’s not a goal of CoW to protect against this.
Previously:
- Swift Copy-on-Write PSA: Mutating Dictionary Entries
- Swift Proposal: Mutability and Foundation Value Types
- Fast Ordered Collections for Swift Using In-memory B-trees
- Exploring Swift Array’s Implementation
Update (2019-02-07): Drew McCormack:
Thanks for summarizing the CoW discussion. I think we are in agreement. I will say, first, that it did surprise me that a ‘let’ constant can mutate, although I accept the explanation. Second, on the question of whether CoW has a race condition, the answer is clearly “Yes”
If I had to summarize that whole exchange, it would be… Me: “Does CoW have a potential race condition?”. Swift Folk: “You shouldn’t be asking that question.” Effectively the question is irrelevant, because you shouldn’t share mutable value types. That is the lesson.
Update (2019-04-16): See also: Ole Begemann’s thread in the Swift forums.