So even using _read
and _modify
cannot fix this synchronization problem. It simply is not possible to lock property mutations in this style, and is why we need to either create one-off methods for mutating state or leverage the modify
method.
This goes to show just how tricky multithreading and data races can be. What seems to be reasonable can often be incorrect and lead to incorrect results. The main problem with locks is they are fully decoupled from the concurrency tool we are using, which in this case is threads. Ideally the locking mechanism has intimate knowledge of how we are running multiple units of work at once in order to guarantee synchronization. This is what Swift’s new concurrency tools provide for us, but before we can discuss that there are a few more things to discuss.
So, Apple’s Thread
class was the primary abstraction people would use on Apple’s platforms in order to unlock asynchrony and concurrency back in the day. It comes with some interesting features, such as priority, cancellation, thread dictionaries and more, but they also lack in many ways:
Threads don’t support the notion of child threads so that things like priority, cancellation and thread dictionaries don’t trickle down to threads created from other threads.
It’s easy to accidentally explode the number of threads being used.
It’s hard to coordinate between threads.
Threaded code looks very different from unthreaded code.
And the tools for synchronizing between threads are crude.
Now there’s a good chance that most of our viewers have never used threads directly in their codebase because ever since macOS Leopard, released 15 years ago, Apple has built abstractions on top of threads to help fix a lot of the problems we just uncovered. This includes operation queues, Grand Central Dispatch and even Combine. Let’s take a look at how those technologies improved upon threads, and see where they fall short.