Home > Software engineering >  What is the reason behind objc_sync_enter doesn't work well with struct, but works well with cl
What is the reason behind objc_sync_enter doesn't work well with struct, but works well with cl

Time:02-03

I have the following demo code.

struct IdGenerator {
    private var lastId: Int64
    private var set: Set<Int64> = []
    
    init() {
        self.lastId = 0
    }
    
    mutating func nextId() -> Int64 {
        objc_sync_enter(self)
        defer {
            objc_sync_exit(self)
        }
        
        repeat {
            lastId = lastId   1
        } while set.contains(lastId)

        precondition(lastId > 0)
        
        let (inserted, _) = set.insert(lastId)
        precondition(inserted)
        
        return lastId
    }
}

var idGenerator = IdGenerator()

class ViewController: UIViewController {

    override func viewDidLoad() {
        super.viewDidLoad()
        // Do any additional setup after loading the view.
    }

    @IBAction func click(_ sender: Any) {
        DispatchQueue.global(qos: .userInitiated).async {
            for i in 1...10000 {
                let id = idGenerator.nextId()
                print("i : \(id)")
            }
        }

        DispatchQueue.global(qos: .userInitiated).async {
            for j in 1...10000 {
                let id = idGenerator.nextId()
                print("j : \(id)")
            }
        }
    }
}

Whenever I execute click, I would get the following crash

Thread 5 Queue : com.apple.root.user-initiated-qos (concurrent)
#0  0x000000018f58b434 in _NativeSet.insertNew(_:at:isUnique:) ()
#1  0x000000018f598d10 in Set._Variant.insert(_:) ()
#2  0x00000001001fe31c in IdGenerator.nextId() at /Users/yccheok/Desktop/xxxx/xxxx/ViewController.swift:30
#3  0x00000001001fed8c in closure #2 in ViewController.click(_:) at /Users/yccheok/Desktop/xxxx/xxxx/ViewController.swift:56

It isn't clear why the crash happen. My raw guess is, under struct, objc_sync_enter(self) doesn't work as expected. 2 threads accessing Set simultaneously will cause such an issue.


If I change the struct to class, everything just work fine.

class IdGenerator {
    private var lastId: Int64
    private var set: Set<Int64> = []
    
    init() {
        self.lastId = 0
    }
    
    func nextId() -> Int64 {
        objc_sync_enter(self)
        defer {
            objc_sync_exit(self)
        }
        
        repeat {
            lastId = lastId   1
        } while set.contains(lastId)

        precondition(lastId > 0)
        
        let (inserted, _) = set.insert(lastId)
        precondition(inserted)
        
        return lastId
    }
}

May I know what is the reason behind? Why the above objc_sync_enter works well in class, but not in struct?

CodePudding user response:

The objc_sync_enter/objc_sync_exit functions take an object instance and use its identity (i.e., address in memory) in order to allocate and associate a lock in memory — and use that lock to protect the code between the enter and exit calls.

However, structs are not objects, and don't have reference semantics which would allow them to be used in this way — they're not even guaranteed to be allocated in a stable location in memory. However, to support interoperation with Objective-C, structs must have a consistent object-like representation when used from Objective-C, or else calling Objective-C code with, say, a struct inside of an Any instance could trigger undefined behavior.

When a struct is passed to Objective-C in the guise of an object (e.g., inside of Any, or AnyObject), it is wrapped up in a temporary object of a private class type called __SwiftValue. This allows it to look like an object to Objective-C, and in some cases, be used like an object, but critically, it is not a long-lived, stable object.

You can see this with the following code:

struct Foo {}
let f = Foo()
print(f) // => Foo()
print(f as AnyObject) // => __SwiftValue

print(ObjectIdentifier(f as AnyObject)) // => ObjectIdentifier(0x0000600002595900)
print(ObjectIdentifier(f as AnyObject)) // => ObjectIdentifier(0x0000600002595c60)

The pointers will change run to run, but you can see that every time f is accessed as an AnyObject, it will have a new address.

This means that when you call objc_sync_enter on a struct, a new __SwiftValue object will be created to wrap your struct, and that object is passed in to objc_sync_enter. objc_sync_enter will then associate a new lock with the temporary object value which was automatically created for you... and then that object is immediately deallocated. This means two major things:

  1. When you call objc_sync_exit, a new object will be created and passed in, but the runtime has no lock associated with that new object instance! It may crash at this point.
  2. Every time you call objc_sync_enter, you're creating a new, separate lock... which means that there's effectively no synchronization at all: every thread is getting a new lock altogether.

This new pointer instance isn't guaranteed — depending on optimization, the object may live long enough to be reused across objc_sync_* calls, or a new object could be allocated exactly in the same place as an old one... or a new object could be allocated where a different struct used to be, and you accidentally unlock a different thread...

All of this means that you should definitely avoid using objc_sync_enter/objc_sync_exit as a locking mechanism from Swift, and switch over to something like NSLock, an allocated os_unfair_lock, or even a DispatchQueue, which are well-supported from Swift. (Really, the objc_sync_* functions are primitives for use largely by the Obj-C runtime only, and probably should be un-exposed to Swift.)

  •  Tags:  
  • Related