Exclusive Memory Access checks too restrictive?

I'm running into a Simultaneous accesses to <ptr>, but modification requires exclusive access issue, which IMO should actually be allowed, bc there are no overlapping accesses going on in the code.

The specific issue (i have a simplified working reproduction example below) is that i have a function which takes an inout parameter (for which we pass a reference to an instance property) and then performs an update operation on a UITableView, which in turn calls the data source's -tableView:numberOfRowsInSection: method, which in turn tries to access the instance property (a reference to which, however, is at this point still being passed to the function, via the inout param).

I get why this is happening (the compiler emits a pair of swift_beginAccess and swift_endAccess calls before and after the call to the inout-param-taking function), but IMO this is overly strict, because the callee doesn't actually access the reference it's passed past a certain point in its body (meaning that the scope of where the callee needs access to the passed reference ends earlier than the scope of the callee as a whole and there is a time period during the execution of the program where the still-ongoing call unnecessarily prevents accesses to the object).

I'm aware that in my specific case, I could work around this by e.g. delaying the table view update until the next layout cycle, but obviously that kind of fix only really works in this specific case, and doesn't really solve the underlying issue (of the exclusive access requirements being too restrictive in the case of inout parameters).

Is this the intended behaviour?


Here is a small example that exhibits this issue:

class TableView {
    var numSectionsProvider: () -> Int = { 0 }

    func toggleAndReload(idx: Int, indices: inout Set<Int>) {
        if indices.contains(idx) {
            indices.remove(idx)
        } else {
            indices.insert(idx)
        }
        // The next line causes the simultaneous access violation,
        // bc the Set is still being accessed by the current function (via the inout param),
        // even though we don't have any further accesses to it beyond the if stmt above.
        // Having the compiler insert the swift_endAccess call here instead of in the caller
        // (after the call) would probably fix this (and have the tracked accesses match
        // the actual accesses more closely).
        let numSections = self.numSectionsProvider()
    }
}

class A {
    let tableView = TableView()
    var collapsedSections = Set<Int>()

    init() {
        tableView.numSectionsProvider = { [weak self] in
            return self?.collapsedSections.count ?? 0
        }
        tableView.toggleAndReload(idx: 0, indices: &collapsedSections)
    }
}

let a = A()

This is intended behavior: the duration of the access to an inout param is notionally the entire call. When passing an inout value you are effectively asserting that the function will have exclusive mutable access to that storage for the duration of the call, and if the compiler can't prove that's the case (as when you have ~arbitrary dynamic accesses thru closures and class references like here) then it will resort to inserting the dynamic checks to make sure there is no overlapping access.

6 Likes

Nitpick: it’s just class fields. Escaping closures cannot capture inout references so there are no additional dynamic checks needed there. Non-escaping closures can capture inout parameters and the compiler can check that statically.

4 Likes

Ah yeah I was thinking a bit more generally about, say, a local mutable variable with some sort of messy closure capture setup that creates an overlapping access. But that's not possible with inout!

1 Like