4

I access a dictionary by key (object), which works fine most of the time, but sometimes it just crashes with:

-[__NSCFNumber objectForKey:]: unrecognized selector sent to instance 0x8000000000000000

The closest question to mine I found was this one.

Here is my (simplified) code:

class Meal {
    private static let KEY = Date()
    private static let KEY_Q = DispatchQueue(label: "Meal.KEY")
    static var menus = OrderedDictionary<Date, [Int:Meal]>()

    static func test() throws {
        var date: Date?
        var menu: [Int:Meal]?
        try KEY_Q.sync {
            menu = Meal.menus[KEY] // <-- Error
            if menu == nil {
                date = KEY.clone()
            }
        }
        DispatchQueue.main.async {
            //This needs to run on the UI Thread, since it also loops over Meal.menus
            if date != nil {
                Meal.menus[date!] = [Int:Meal]()
            }
        }
    }
}

class Date: Hashable & Comparable {
    var days = 0
    func hash(into hasher: inout Hasher) {
        hasher.combine(days)
    }

    func clone() -> Date {
        let date = Date()
        date.days = days
        return date
    }
}

class OrderedDictionary<keyType: Hashable, valueType>: Sequence {
    var values = [keyType:valueType]()

    subscript(key: keyType) -> valueType? {
        get {
            return self.values[key]
        }
    }
}

Note:

  • Entries are added to menus from the UI Thread, while my code is running in a different thread.
  • The keys stored in the dictionary are clones of KEY (not references to KEY)
  • I think the error might have started occuring with the migration to Swift 5 and therefore hash(into hasher: inout Hasher)

Questions:

  • Is a Swift dictionary thread safe for insertion and access?
  • How would I lock the KEY object and the UI Thread?
  • Is hash(into hasher: inout Hasher) implemented correctly?
  • Why does this error occur and how can i fix it?
Minding
  • 1,383
  • 1
  • 17
  • 29
  • 1
    If you also wrap just the `Meal.menus[date!] = [Int:Meal]()` statement in a `KEY_Q.sync { .. }`, does the problem go away? – Chris Aug 31 '19 at 13:23
  • @Chris I'll try, but since the error occurs randomly I won't be able to say for sure. – Minding Aug 31 '19 at 13:29
  • @Chris Seems to be working, you can post it as an answer. – Minding Aug 31 '19 at 13:37
  • Consider restructuring your code to eliminate the `static var menus` and then you can run your `test()` many times in a loop, creating a fresh object every time. (A `Restaurant` with menus passed in at initialisation?) – Chris Aug 31 '19 at 13:38

1 Answers1

6

This does very much look like a threading problem (that address 0x8000000000000000 is very suspect), so you could serialise writes in the same way you have reads:

    DispatchQueue.main.async {
        //This needs to run on the UI Thread, since it also loops over Meal.menus
        if let date = date {
            KEY_Q.sync {
                Meal.menus[date] = [Int:Meal]()
            }
        }
    }

To avoid the potential for inadvertently reintroducing the bug elsewhere, you could also consider wrapping the queue and the accesses in a little helper object (such that accesses could only happen on the right queue).

Chris
  • 3,445
  • 3
  • 22
  • 28
  • 1
    Don't check for nil and force unwrap. Instead, use conditional binding. – Alexander Aug 31 '19 at 15:24
  • 1
    @Alexander That was inherited from the question, but good point. Fixed. – Chris Sep 01 '19 at 10:39
  • Can you explain why 800000000 is suspicious? I have a similar issue and it's indeed threading issue, but not sure why 8000000000 –  Aug 13 '21 at 22:46
  • @John There's probably a specific meaning to it only having the very highest bit set, but I couldn't tell you what that is. If you look at addresses of other vars, you'll soon appreciate that 800000000 sticks out a mile. I suggest you try to concisely reproduce your problem and post it as a new question here. – Chris Aug 22 '21 at 13:47