Potential of race condition in ARC?

I ran into a memory issue that I don't understand why this could happen. For me, It seems like ARC doesn't guarantee thread-safety.

Let see the code below

@propertyWrapper
public struct AtomicCollection<T> {
    private var value: [T]
    private var lock = NSLock()
    
    public var wrappedValue: [T] {
        set {
            lock.lock()
            defer { lock.unlock() }
            value = newValue
        }
        get {
            lock.lock()
            defer { lock.unlock() }
            return value
        }
    }
    
    public init(wrappedValue: [T]) {
        self.value = wrappedValue
    }
}

final class CollectionTest: XCTestCase {
    func testExample() throws {
        let rounds = 10000
        let exp = expectation(description: "test")
        exp.expectedFulfillmentCount = rounds
        
        @AtomicCollection var array: [Int] = []
        for i in 0..<rounds {
            DispatchQueue.global().async {
                array.append(i)
                exp.fulfill()
            }
        }
        
        wait(for: [exp])
    }
}

It will crash for various reasons (see screenshots below)

I know that the test doesn't reflect typical application usage. My app is quite different from traditional app so the code above is just the simplest form for proof of the issue.

One more thing to mention here is that array.count won't be equal to 10,000 as expected (probably because of copy-on-write snapshot)

So my questions are

  1. Is this a bug/undefined behavior/expected behavior of Swift/Obj-c ARC?
  2. Why this could happen?
  3. Any solutions suggest?
  4. How do you usually deal with thread-safe collection (array, dict, set)?

Answered by DTS Engineer in 814878022
It would be interesting to see the implementation of append.

I suspect that append(…) will normally use the _modify accessor but that can’t happen in this case because of the property wrapper. Swift Evolution is starting to look at how to standardise this stuff; see [Pitch] Modify and read accessors. However, that’s not really germane to this issue.

It will crash for various reasons.

That’s because you’re not following Swift’s concurrency rules. I put your code into a small test project and compiled it with Xcode 16.1 and the compiler tells you what the problem is:

    array.append(i)
 // ^ Mutation of captured var 'array' in concurrently-executing code

Your code isn’t legal because it doesn’t follow the “Law of Exclusivity”. I have a link to the Swift Evolution proposal that explains that in my Swift Concurrency Proposal Index post.

Taking a step back, using a property wrapper for locking is most definitely an anti-pattern. It’s better — in that it’s easier, more efficient, and actually works (-: — to use a payload-carrying lock for that. If you can rely on the latest OS releases, the lock of choice is Mutex. If you can’t, use OSAllocatedUnfairLock. If you have to deploy way back, you can write your own lock that has similar semantics.

For example, this runs without crashing:

import Foundation
import os

func testExample() {
    let rounds = 10000
    
    let array = OSAllocatedUnfairLock<[Int]>(initialState: [])
    for i in 0..<rounds {
        DispatchQueue.global().async {
            array.withLock { a in
                a.append(i)
            }
        }
    }
    
    dispatchMain()
}

testExample()

Share and Enjoy

Quinn “The Eskimo!” @ Developer Technical Support @ Apple
let myEmail = "eskimo" + "1" + "@" + "apple.com"

Interesting.

Presumably append is calling the getter and setter. Presumably the getter is not copying the entire array. But the Swift Array is a value-type (i.e. a struct), not an objC-compatible class, right? So I think this relies on some new Swift copy-on-write magic, not objC-style ARC, right?

It would be interesting to see the implementation of append.

It would be interesting to see the implementation of append.

I suspect that append(…) will normally use the _modify accessor but that can’t happen in this case because of the property wrapper. Swift Evolution is starting to look at how to standardise this stuff; see [Pitch] Modify and read accessors. However, that’s not really germane to this issue.

It will crash for various reasons.

That’s because you’re not following Swift’s concurrency rules. I put your code into a small test project and compiled it with Xcode 16.1 and the compiler tells you what the problem is:

    array.append(i)
 // ^ Mutation of captured var 'array' in concurrently-executing code

Your code isn’t legal because it doesn’t follow the “Law of Exclusivity”. I have a link to the Swift Evolution proposal that explains that in my Swift Concurrency Proposal Index post.

Taking a step back, using a property wrapper for locking is most definitely an anti-pattern. It’s better — in that it’s easier, more efficient, and actually works (-: — to use a payload-carrying lock for that. If you can rely on the latest OS releases, the lock of choice is Mutex. If you can’t, use OSAllocatedUnfairLock. If you have to deploy way back, you can write your own lock that has similar semantics.

For example, this runs without crashing:

import Foundation
import os

func testExample() {
    let rounds = 10000
    
    let array = OSAllocatedUnfairLock<[Int]>(initialState: [])
    for i in 0..<rounds {
        DispatchQueue.global().async {
            array.withLock { a in
                a.append(i)
            }
        }
    }
    
    dispatchMain()
}

testExample()

Share and Enjoy

Quinn “The Eskimo!” @ Developer Technical Support @ Apple
let myEmail = "eskimo" + "1" + "@" + "apple.com"

Potential of race condition in ARC?
 
 
Q