Simultaneous accesses to ... , how am i getting this on swift 5.8?

well this is really weird:

Simultaneous accesses to 0x7f606800d9f0, but modification requires exclusive access.
Previous access (a modification) started at <unavailable> + 8528498 (0x5595ba0a6272).
Current access (a read) started at:

now first of all, i know what the exception “is”, but how on earth am i getting this on swift 5.8? i am not using any unsafe concurrency, and i have StrictConcurrency enabled in build settings.

when i capture the stack trace in a debugger, i see it is happening in a _read accessor of the following computed property:

final
class Inliner
{
    private
    var cache:InlinerCache

    private
    init(cache:InlinerCache)
    {
        self.cache = cache
    }
}
extension Inliner
{
    var masters:InlinerCache.Masters
    {
        _read   { yield  self.cache.masters }
//      ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ swift_beginAccess
        _modify { yield &self.cache.masters }
    }
}

this class is not Sendable and it never crosses a concurrency domain. how is an exclusivity violation even possible here?

I mean, almost trivially:

func bad(_ masters: inout InlinerCache.Masters) {
  print(self.masters)
}
self.bad(&self.masters)

If I recall correctly, TSan will catch single-threaded exclusivity violations, so you can get backtraces if you haven’t already. But I guess not if this is coming from production.

2 Likes

I think the exclusivity errors predate Swift 5.8 and concurrency by quite a bit. They started in ~Swift 4 and might have upgraded some warnings to errors in later versions.

1 Like

ah, i was so used to the overlapping access diagnostic you get when you call a mutating method on a struct i forgot this can still happen in a class type.

i can reproduce the crash, so i get a stack trace, so i’m currently trying to symbolicate the Previous access (a modification) started at error, but symbolicate-linux-fatal doesn’t work on docker (along with the REPL) due to missing lldb…

okay, so it turns out i was calling a mutating method on the self.cache, and inside its closure parameter, i was accessing the self.masters computed property, like:

extension Inliner
{
    func uri(x:Int) -> String
    {
        self.cache.load(x) { "\(URI.init(self.masters[x], $0))" } 
    }
}

when i moved the implementation to the InlinerCache struct (where it really belongs), i got the overlapping access diagnostic:

extension InlinerCache
{
    mutating
    func uri(x:Int) -> String
    {
        self.load(x) { "\(URI.init(self.masters[x], $0))" } 
    }
}

so, lesson learned, never yield access to a stored class property.

but interestingly, if i spell out the key path like:

extension Inliner
{
    func uri(x:Int) -> String
    {
        self.cache.load(x) { "\(URI.init(self.cache.masters[x], $0))" } 
    }
}

i still do not get an overlapping access diagnostic, even though the compiler ought to be able to statically detect overlapping access to the same property of the class. can we improve the diagnostics here?

2 Likes

I assume cache is also a class, which means the overlapping access to cache isn't the problem: it's the overlapping access to cache.masters. And that's not visible in your code (without cross-function analysis). Nope, it's a struct, you said so and showed so above.

I think as long as the closure argument is non-escaping, the compiler can conservatively assume it'll be invoked during the access (if it definitely won't be, you can replace the body with fatalError("shouldn't be called")), and that means it should be able to analyze this. It's possible that that still counts as cross-functional analysis at the SIL level, which means a bit of extra work, but it should be doable.

2 Likes