Wednesday, February 6, 2019

Why Swift’s Copy-on-Write Is Safe

Drew McCormack:

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?

Joe Groff:

It’s thread safe to read and copy but not write (modulo bugs). It should be as thread safe as an int variable

Joe Groff:

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. The isUniquelyReferenced is itself threadsafe

Drew McCormack:

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.

Joe Groff:

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”

Daniel Jalkut:

I think Drew is not concerned with isUnique’s atomicity, but with the atomicity of the return/copy code that follows.

Joe Groff:

isUnique takes its argument inout intentionally to ensure this isn't a problem. Swift's inout 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 reference

In 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.

Helge Heß:

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) )

Drew McCormack:

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.

Raphael Sebbe:

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:

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.

1 Comment RSS · Twitter

Leave a Comment