I've been working on a Gameboy emulator as a little project and I ran into my first "simultaneous accesses to 0x6000deadbeef, but modification requires exclusive access" error.
At first I was confused as this is a purely single-threaded affair. But after doing some reading on the Swift blog I think I got my head around the basics of what's going on. I've boiled down the code to this simple reproduction:
// This is `HardwareRegisters` in the real code.
// It contains things like timers and the current scanline.
struct A {
var bus: Bus!
var value = 0
// This is a function that maps writes from games to the
// hardware registers. Writes to these values from 'user' code
// are often special, behaving in different ways.
mutating func update() {
value += 1
// In this case, when the timer clock is changed,
// the timer needs to reset itself based on another
// hardware register.
if value == 1 {
bus.b.reset()
}
}
}
// This is `Timer` in the real code.
// It periodically increments a hw register and signals an
// interrupt.
struct B {
var bus: Bus!
var v = 0
mutating func reset() {
v += bus.a.value
}
}
// The Bus contains all the hardware components -- CPU, Memory, PPU, etc.
class Bus {
var a = A()
var b = B()
init() {
a.bus = self
b.bus = self
}
func write() {
a.update()
}
}
var bus = Bus()
bus.write()
If my understanding is correct, as the call to a.update() is a mutating one, that call starts a modification and until that function has returned a new modification or read cannot be started.
So far so reasonable. But what I'm having a little difficulty understanding is if I naïvely inline B::reset:
// In this case, when the timer clock is changed,
// the timer needs to reset itself based on another
// hardware register.
if value == 1 {
//bus.b.reset()
bus.b.v += value
}
There's no problem. Is the second variation somehow 'safer', due to not going through the a property to get at value? Or is this more a side-effect of the way this error has been implemented?
My options at this point are either inline the call (slightly more complicated than the above example, but it's not too bad) or make A a class. Is either obviously better to folks?
Is the second variation somehow 'safer', due to not going through the a property to get at value ?
Yes. Your inline preserves exclusivity because the read is happening byA of A's own value property. It's fine for A to read and mutate itself inside of its own mutating function.
What's not safe is for other code from a different context that's not A to try and readA while A is in the middle of mutating itself: that would make it easy for other code to read A while A is in an inconsistent state, and that's what Swift is trying to protect you from.
While you could definitely switch to a class to get this to work, or even pass values directly to your reset() call, it may be better to make Bus responsible for updating these values, rather than integrating side-affects into your mutating methods. A single mutation inside of Bus would consolidate things a bit.
So I think I understand that from a broad perspective. In this particular case, I don't think there's a way for other code (reset) to see A in an inconsistent state -- I'm assuming Swift is taking a broad-brush approach, and isn't spending time working out if a particular access is safe. (Which I assume is difficult, computationally expensive, and/or impossible.)
Apologies if this all sounds a little obvious -- I'm trying to get my head around 'the rules' -- which seem to be "no external access while a modification is in progress"?
There are probably other reasons why you don't want to try and do this, to be honest. The semantics around value types and how explicit they are about mutation and exclusivity are a sort of contract that Swift can use to optimize your code if it knows exactly when mutations and whatnot are happening. If you break those assumptions you may open yourself up to other bugs when things about Swift change, so it's worth abiding by the contract. But someone more knowledgable than me could probably say more about that.
Basically, I'd say. I'm not sure you would want it any other way — mutating func is a promise you're making, and asking Swift to get really smart and realize "oh actually you're done mutating before this function has exited" could get messy.
On a related note, taking advantage of var a: A { didSet { /* ... */ } } is another option for doing what you're trying to do here, I think — you'd no longer be in the context of a mutation, but still able to trigger side-affects that read A.