Value Semantics, Copy-on-Write, and Multi-Threading went into a Bar…

TL;DR When you pass data between threads, whether you are using value types or not, you can very easily run into undefined behavior with unexpected results.

When we first heard about Swift structs, and Apple was busy convincing us to adopt value types wherever possible, one of the main selling points was thread safety. When you work with a value type, it is like your own personal copy of the value, and you don’t have to worry that some other part of the code will mutate it. We have all heard this argument many times, and I personally never really questioned the wisdom.

After an extensive discussion on Twitter, my understanding has changed. Although it is no doubt unlikely that you will be bitten by threading issues when working with value types, you can be, and when that happens, very odd things can happen.

Basically, any time you try to access a mutable value from multiple threads, you can already start to run into issues. Assume, for example, one thread is updating a var, while another thread attempts to copy it into a new let constant.

Here we begin on the main thread, initialize the variable s, and then shoot off a block to a background queue, where we attempt to copy the value of s into a new let constant (s2). In the meantime, back on the main thread, s is being set to a different value.

It should be clear there is a race condition here, and the result of the print cannot be determined. In one run, s2 may be initialized before s is mutated, so that the console shows i = 1, j = 2. In another run, perhaps the mutation takes place first, and s2 ends up holding i = 3, j = 4.

For anyone with experience working in multithreaded programming environments, this will come as no surprise. This type of race condition is well understood. But, it can get creepier…

In theory, s2 may not end up with either of the values above. It could, for example, end up being i = 1, j = 4. It all depends on how far the main thread gets before the copy is made. If main thread execution is half way through setting the properties when the copy grabs the memory. This would lead to a “mixed” value like i = 1, j = 4.

When you add Copy-on-Write, things can get even stranger. CoW is used extensively in Swift, primarily as a way to avoid expensive copying of value types like Array and Dictionary. And if you are using your own deeply nested or large structs, you are generally advised to look at implementing CoW yourself.

Here’s an example of a CoW type, and code to induce a race condition. You can run it just by dropping it into a Playground.

When you run this, the saneOperator constant will actually change right underneath you. The first print will issue “We are safe”, but printing exactly the same constant a bit later will write out “We are in danger”. Remember — that’s a let constant with value semantics that is spontaneously changing.[1]

When I first saw this, I thought the sky was falling. If we can’t trust a value type to exhibit value semantics — and a let value type should not be able to change value — then what can we trust?

It’s not that bad, of course. I’m assured that this is all due to doing something illegal in the first place, namely accessing the same mutable value type across threads. Once you do that, all bets are off — behavior is “undefined”. Value constants can mutate…mixed values can arise which never previously existed…the sky can fall.

So what’s the lesson here? First, it is not true that value types are immune to threading issues. If you had that impression — and you would be forgiven for believing it — you need to tone down your expectations. Because the data in value types gets copied around a lot more than for classes, there is less of a risk that two threads will contend it, but it is no guarantee. To guard against it altogether, try to make immutable copies of your data types, whether they are structs or classes, before you fire them across a thread boundary. Avoid sharing mutable data between threads.

[1] I am told by the experts that this behavior — a let constant spontaneously mutating — can also happen for large non-CoW structs. Apparently it is all about how memory reads and writes get scheduled between threads. So this is not just a CoW problem, but we can at least easily simulate the issue using CoW.

Master of none. @drewmccormack