mikeash.com: just this guy, you know?

Posted at 2007-02-08 00:00 | RSS feed (Full text feed) | Blog Index
Next article: How To Shrink Your Source Code
Previous article: Why CoreAudio is Hard
Tags: autorelease bug cocoa objectivec
More Fun With Autorelease
by Mike Ash  

I just hit a subtle but commonly known bug for the first time. I thought I'd share my fun with the world. Everybody reading this blog should know about autorelease pools and how they work in Cocoa. As everybody knows, every time you go through the event loop, Cocoa blows away the old pool and makes a new one for you, so that all of your autoreleased objects go away and your new ones go into a fresh pool. That way you never build up more objects than get produced during a single event loop cycle.



The key word is "event loop". In Apple's infinite wisdom, things that aren't real actual NSEvents don't trigger the pool.

I'm currently working on an app that spends a lot of time in the background doing dark, unspeakable things with NSStreams on the main thread. I encountered a bug where one of my objects can get destroyed in the middle of handling a stream event, which left it open to getting other stream events after it was deallocated. (Apparently NSStreams can still send you stream events even after you've closed them, released them, and set their delegate to nil, but that's an entirely different problem for another day.)

The obvious fix was to simply do [[self retain] autorelease] before making the problem call. And fix it it did, except instead of my dealloc happening in the middle of my event handler, it never happened at all.

Until I clicked on my app's dock icon.

At least the solution was easy. Post an NSApplicationDefined event in the stream event handler, and autoreleased objects get destroyed on schedule.

The amazing part is that, from what I've heard, this bug has been there for a long time. I haven't used it, but it seems like CFRunLoopObserver would make it trivial to hook into the runloop at such a low level that you could drain the current pool if anything so much as blinks. And as we all know, Autorelease is Fast, so there should be no penalty in doing this.

How many of our apps sit quietly ticking away in the background, accumulating ever larger autorelease pools that only get drained once we bring them forward? It makes you wonder.

Hopefully this will be fixed in Leopard. I'm afraid to file a bug lest I get a "Behaves Correctly".
Did you enjoy this article? I'm selling a whole book full of them. It's available for iBooks and Kindle, plus a direct download in PDF and ePub format. It's also available in paper for the old-fashioned. Click here for more information.

Comments:

Lee Falin at 2007-02-09 14:33:00:
But can you imagine how many apps would die a sudden death if this were fixed.

Charles at 2007-11-28 00:59:48:
@Lee: why would the apps die? Enlighten me. This seems like a huge one to miss. Surely the folk at Apple are like me and have 20-odd apps running which they might visit for days? Don't they?

mikeash at 2007-11-28 01:55:02:
It's very common for apps to start relying on undocumented bits of memory management, whether intentionally or otherwise.

For example, it was common on old systems that calling free() on a block of memory would not actually alter that memory until, at the soonest, the next call to malloc(). I believe some systems actually documented and guaranteed this. So a lot of programs ended up relying on being able to modify pointers to freed memory if it was done immediately. Some did it intentionally, some did it by accident. When those programs were moved to systems without that guarantee, they failed.

This autorelease thing is similar. The API only guarantees that autoreleased objects will survive at least until the caller of your method regains control (or an explicit autorelease pool pop occurs). However, in this case, autoreleased objects may persist beyond the current event and still be around when things like timers or delayed performs occur. Relying on them to be there is a bug, but it's the sort of bug which a lot of apps could have.

Charles at 2007-11-28 21:43:48:
Relying on them to be there is a bug, but it's the sort of bug which a lot of apps could have.

Could you be any more explicit about what function you'd use relying on something like that? I'm just intrigued. And also not anything near an expert in C++.

mikeash at 2007-11-28 23:28:31:
Well, imagine some code like this:

someIvar = [obj autoreleasedString]; // gets destroyed next event
   [self performSelector:@selector(continuation) withObject:nil afterDelay:0];

- (void)continuation {
   [obj doSomethingWith:someIvar];
}


It's possible this code will work correctly with the current situation, since the autorelease pool isn't destroyed until the next event, and the delayed perform doesn't qualify. Whereas if the problem were fixed to destroy the pool every time control returned to the runloop, the object in someIvar would be destroyed before continuation can execute and this code will crash.

Charles at 2007-11-29 04:24:34:
...and what sort of circumstances might you use code like that? What would it do? How would I, the dumb (in many senses) user, see it work in the situation where you have the bug-that-may-not-be-a-bug? Or is it lazy coding, and one should always expect that the pool gets destroyed?

mikeash at 2007-11-29 04:54:46:
You'd see code like that in circumstances where the programmer screwed up. The user would either see it function correctly or see the program crash, depending on whether the object is destroyed early or not. It is most likely a simple mistake, as things like this often don't get caught until they actually begin to cause problems.

Stripes at 2008-07-08 01:40:31:
For example, it was common on old systems that calling free() on a block of memory would not actually alter that memory until, at the soonest, the next call to malloc(). I believe some systems actually documented and guaranteed this.


The original K&R C book (which served many as Unix documentation as well a C documentation) said you could use the most recently free'ed memory block until the next malloc or free. It said it without a word of warning, and used it in an example. So it was clearly promoted as "good practice" at some point.

Which I wouldn't say is nearly as bad as what many C++ books promote as good practice :-)

Lord Anubis at 2010-04-06 18:52:00:
Do you know if it did get fixed n 10.5 or 10.6?

Thanks for your Blog postings. Most are enlightened.
Thanks

mikeash at 2010-04-06 19:33:05:
I recently ran into this on 10.6, so it hasn't been fixed yet. Too bad!

Robert Monaghan at 2010-10-25 12:17:41:
Hi Mike,

I have a QuickTime component, with a bunch of Obj-C objects inside. (ie: a 32-bit Core Foundation bundle.) (I am doing the work needed to switch over to the future Cocoa based media framework.)

Should I place a CFRunLoopObserver in there, to trigger a drain? I do see some accumulation when run through ObjectAlloc. (And a reasonable amount of Obj Leakage, too.)

Would a CoreFoundation Framework behave as a "Good Citizen" when doing this?

In other words, how do I get my CoreFoundation framework with embedded Obj-C objects to flush the autorelease pool, thus eliminating leakage?


bob.

mikeash at 2010-10-25 18:11:17:
Is it even guaranteed for there to be an active runloop in the thread where you're invoked?

Your best bet is probably to just surround all of your component's entry points with manually-created autorelease pools.

Robert Monaghan at 2010-10-27 14:15:49:
I believe that you are right. I can't be certain that there will even be a runloop. (Especially with Command line based Quicktime tools.)

I have done that already, but Leaks does report some leakage.
I will dig around a bit more to see what is going on.

Thanks!

bob.

mikeash at 2010-10-27 17:02:33:
If you're leaking due to not having an autorelease pool, it will be logged, so your problem is probably something else.

Karl at 2010-12-08 15:33:00:
I filed a bug with Apple over this, 8743767. I'll see what they say.

Karl at 2010-12-08 15:39:02:
By the way, if it saves anyone else time, my workaround is:

- (void)_kickEventLoop
{
     NSEvent *event = [NSEvent otherEventWithType:NSApplicationDefined
                 location:NSMakePoint(0.0, 0.0) modifierFlags:0
                 timestamp:0 windowNumber:0 context:nil
                 subtype:0 data1:0 data2:0];
     [NSApp postEvent:event atStart:FALSE];
}

and call that from notification handlers.

it4rb at 2015-04-08 06:52:03:
It have been 7 years on, and this bug still happen in Yosemiteeeeeeeee, yay!


Comments RSS feed for this page

Add your thoughts, post a comment:

Spam and off-topic posts will be deleted without notice. Culprits may be publicly humiliated at my sole discretion.

Name:
Web site:
Comment:
Formatting: <i> <b> <blockquote> <code>. URLs are automatically hyperlinked.
Hosted at DigitalOcean.