Friday, January 24, 2025

Debug Descriptions on the Wrong Thread

Douglas Hill (Bluesky):

Is there a best practice for implementing description and debugDescription for main actor classes with Swift strict concurrency?

Currently, he’s checking the current thread and not trying to read the object if it’s the wrong one.

I’ve run into a similar issue with Core Data. Managed objects are supposed to be confined to a single queue, but it’s easy for them to leak, as they get included in the user info dictionaries of notifications and errors. If you try to log an error you’ll get undefined behavior or a crash if com.apple.CoreData.ConcurrencyDebug is enabled.

Maybe it’s OK to override description and dispatch it to the correct queue. The object knows its own context, after all, and the context knows the queue. But I’ve always had the suspicion that performing within description could cause a deadlock or something. So, instead, I sometimes try to catch Core Data errors and generate/store the description before they cross the thread boundary.

6 Comments RSS · Twitter · Mastodon


I don’t use Core Data but shouldn’t Core Data copy the info for identifying the managed object related to an error in a separate object and use that for populating the error’s userinfo instead of using the actual managed object?


@ObjC4Life The objectID property can be referenced from any thread, so you have the info that you need. You just have to be careful not to log the error from a different thread because that will call description on the managed objects. Arguably, Core Data should copy more info at the time the error is generated to make the error itself threadsafe. But that could have unwanted performance and memory implications.


Simply logging an error shouldn’t cause a crash, ever IMO. I wouldn’t expect a call to description to be the source of a bug, unless the -description method was being used in a very nonstandard way.

But if Apple insists on being this strict about memory and “safety” then the cost of the copy must be paid. The entire user info dict in all errors must be copied when an error is created.

calling disparch_sync to the “correct” queue as a workaround sounds very bad to me. Like you mentioned I could see that workaround to be the cause of a lot of deadlocks.


@ObjC4Life I’ve only seen it crash in this situation because of the debug mode (which tells it to crash on a threading violation). But, obviously, that can get in the way of debugging. And who knows what happens in regular mode when description does stuff with the context from the wrong thread?


Well i think the userinfo should include a minimal copy, not the entire object, box it in another object perhaps. Obviously that isn’t controlled by us in the case of Core Data since that’s Apple’s cose. Not sure what kind of string core data uses under the covers but if it’s immutable, “copying” it just returns the same instance so it shouldn’t cost too much to copy whatever is needed for the error user info.


So Douglas wrote:

"Is there a best practice for implementing description and debugDescription for main actor classes with Swift strict concurrency? This works, but seems off:
override var description: String {
if Thread.isMainThread {
MainActor.assumeIsolated {
"\(type(of: self)) Property 1: \(property1), Property 2: \(property2)"
}
} else {
"\(type(of: self))(Not on main thread)"
}
}
"

If you are only calling readonly immutable getters in your description it should be safe to call from any thread. I don't do Swift/Swift Concurrency but is there not a way to tell the compiler that this method call can be called on any thread? If the class is mutable and has mutable properties that you want to be included within the description method I would opt for this:

dispatch_async(dispatch_get_main_queue(), ^{
NSLog(@"Error: %@",[someObj description]);
});

Over doing dispatch_sync from within -description and putting it on the main queue.

Leave a Comment