does NSThread create autoreleasepool automatically now?
Asked Answered
P

2

10

I have test code like this

- (void)viewDidLoad
{
    [super viewDidLoad];
    NSThread *thread = [[NSThread alloc] initWithTarget:self selector:@selector(test) object:nil];
    [thread start];
}

-(void)test
{
    MyClass *my = [[[MyClass alloc] init] autorelease];
    NSLog(@"%@",[my description]);
}

I did not create any autoreleasepool for my own thread, but when the thread exit, object "my" just dealloc.why?

even though I change my test code as below

- (void)viewDidLoad
{
    [super viewDidLoad];

    NSThread *thread = [[NSThread alloc] initWithTarget:self selector:@selector(test) object:nil];
    [thread start];
} 

-(void)test
{
    NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
    MyClass *my = [[[MyClass alloc] init] autorelease];
    NSLog(@"%@",[my description]);
}

I create my own autoreleasepool but not drain it when the thread exit. object "my" can still dealloc anyway. why?

I use Xcode5 and not using ARC

Pastor answered 25/7, 2014 at 9:35 Comment(3)
How are you saying your my object get released?Roadwork
are you able to get required information?Roadwork
because i have a breakpoint in object "my"'s dealloc methodPastor
G
31

It's not documented, but the answer appears to be Yes, on OS X 10.9+ and iOS 7+.

The Objective-C runtime is open-source so you can read the source to see what's going on. The latest version of the runtime (646, which shipped with OS X 10.10 and iOS 8) does indeed add a pool if you perform an autorelease without a pool on the current thread. In NSObject.mm:

static __attribute__((noinline))
id *autoreleaseNoPage(id obj)
{
    // No pool in place.
    assert(!hotPage());

    if (obj != POOL_SENTINEL  &&  DebugMissingPools) {
        // We are pushing an object with no pool in place, 
        // and no-pool debugging was requested by environment.
        _objc_inform("MISSING POOLS: Object %p of class %s "
                     "autoreleased with no pool in place - "
                     "just leaking - break on "
                     "objc_autoreleaseNoPool() to debug", 
                     (void*)obj, object_getClassName(obj));
        objc_autoreleaseNoPool(obj);
        return nil;
    }

    // Install the first page.
    AutoreleasePoolPage *page = new AutoreleasePoolPage(nil);
    setHotPage(page);

    // Push an autorelease pool boundary if it wasn't already requested.
    if (obj != POOL_SENTINEL) {
        page->add(POOL_SENTINEL);
    }

    // Push the requested object.
    return page->add(obj);
}

This function is called when you push the first pool (in which case the thing pushed is POOL_SENTINEL), or you autorelease with no pool. When the first pool is pushed, it sets up the autorelease stack. But as you see from the code, as long as the DebugMissingPools environmental variable is not set (it's not set by default), when autorelease is done with no pool, it also sets up the autorelease stack, and then pushes a pool (pushes a POOL_SENTINEL).

Similarly, (it's a little hard to follow without looking at the other code, but this is the relevant part) when the thread is destroyed (and the Thread-Local Storage is destroyed), it releases everything in the autorelease stack (that's what the pop(0); does) so it doesn't rely on the user to pop the last pool:

static void tls_dealloc(void *p) 
{
    // reinstate TLS value while we work
    setHotPage((AutoreleasePoolPage *)p);
    pop(0);
    setHotPage(nil);
}

The previous version of the runtime (551.1, which came with OS X 10.9 and iOS 7), also did this, as you can see from its NSObject.mm:

static __attribute__((noinline))
id *autoreleaseSlow(id obj)
{
    AutoreleasePoolPage *page;
    page = hotPage();

    // The code below assumes some cases are handled by autoreleaseFast()
    assert(!page || page->full());

    if (!page) {
        // No pool. Silently push one.
        assert(obj != POOL_SENTINEL);

        if (DebugMissingPools) {
            _objc_inform("MISSING POOLS: Object %p of class %s "
                         "autoreleased with no pool in place - "
                         "just leaking - break on "
                         "objc_autoreleaseNoPool() to debug", 
                         (void*)obj, object_getClassName(obj));
            objc_autoreleaseNoPool(obj);
            return nil;
        }

        push();
        page = hotPage();
    }

    do {
        if (page->child) page = page->child;
        else page = new AutoreleasePoolPage(page);
    } while (page->full());

    setHotPage(page);
    return page->add(obj);
}

But the version before that (532.2, which came with OS X 10.8 and iOS 6), does not:

static __attribute__((noinline))
id *autoreleaseSlow(id obj)
{
    AutoreleasePoolPage *page;
    page = hotPage();

    // The code below assumes some cases are handled by autoreleaseFast()
    assert(!page || page->full());

    if (!page) {
        assert(obj != POOL_SENTINEL);
        _objc_inform("Object %p of class %s autoreleased "
                     "with no pool in place - just leaking - "
                     "break on objc_autoreleaseNoPool() to debug", 
                     obj, object_getClassName(obj));
        objc_autoreleaseNoPool(obj);
        return NULL;
    }

    do {
        if (page->child) page = page->child;
        else page = new AutoreleasePoolPage(page);
    } while (page->full());

    setHotPage(page);
    return page->add(obj);
}

Note that the above works for any pthreads, not just NSThreads.

So basically, if you are running on OS X 10.9+ or iOS 7+, autoreleasing on a thread without a pool should not lead to a leak. This is not documented and is an internal implementation detail, so be careful relying on this as Apple could change it in a future OS. However, I don't see any reason why they would remove this feature as it is simple and only has benefits and no downsides, unless they completely re-write the way autorelease pools work or something.

Grandchild answered 29/5, 2015 at 1:35 Comment(10)
Outstanding answer. Great detective work. Thanks for posting. (Voted)Tender
Note that the automatic cleanup does not happen for the main thread. The cleanup function tls_dealloc is a thread-local-storage (TLS) destructor, added by a call to pthread_key_init_np. These destructors are called from pthread_exit, which gets implicitly called when a thread returns from its start routine, but not in the case of the main thread. So the implicit pool added by autoreleaseNoPage will not be drained. See pubs.opengroup.org/onlinepubs/9699919799/functions/…Selfrespect
Confirmed by Quinn: developer.apple.com/forums/thread/114025Upgrade
Fwiw you can test this via ``` #import <Foundation/Foundation.h> int main(int argc, char *argv[]) { NSString *base = [NSString stringWithFormat:@"%d %d", 2, 2]; NSLog(@"%p", base); NSString * date2 = [[base retain] autorelease]; NSString * date3 = [[[NSString stringWithFormat:@"%d %d", 2, 3] retain] autorelease]; NSLog(@"Test %@ %@", date2, date3); NSLog(@"%p %p", date2, date3); [NSAutoreleasePool showPools]; sleep(100); } ``` on 10.9, it only complains when OBJC_DEBUG_MISSING_POOLS=YES as you notedUpgrade
But even more interestingly and the reason I revisited this is that the same code on really modern osx (os 14) seems to skip the autorelease entirely, (of course compiled with -fno-objc-arc and -O0). OBJC_DEBUG_MISSING_POOLS=YES also has no effect. So there's something really interesting going on thereUpgrade
@1110101001: Can you give an example where it "skips the autorelease"?Grandchild
@Grandchild If I build/run the snippet posted in that comment on osx sonoma, I see that the call to showPools prints 0 releases pending even though there should be at least 4 autoreleases queued. I can see from disassembly that there is at least the explicit call to autorelease, so either showPools output is broken on modern osx, or objc runtime does something smart. If I use an explicit autorelease pool, the output only shows 1 autorelease queued, that is the pool itself.Upgrade
Note that what I'm referring to here is completely different from the traditional "skip the autorelease" which is done when both caller/callee have ARC enabled, and which case ownership can be transferred directly without needing to use an intermediary autorelease. That part is relatively well documented by others, so you can get info there. I'm not experienced enough to dig into the objc runtime source to figure out what I'm seeing here on modern osx, maybe someone smarter than me can investigate this.Upgrade
@1110101001: It turns out that the strings you had had runtime class NSTaggedPointerString, so basically all the string data was stuffed into the pointer itself, and there was no dynamically-allocated memory that needed to be reference-counted. When I made the string longer so it wouldn't fit in a tagged pointer, I see the pending releases.Grandchild
@Grandchild Brilliant observation! Indeed tagged nsstrings were added in 10.10 (mikeash.com/pyblog/…) so this explains the discrepancy. Thank you for solving the mystery!Upgrade
W
1

Apple documentation says (4th paragraph):

You create an NSAutoreleasePool object with the usual alloc and init messages and dispose of it with drain (or release—to understand the difference, see Garbage Collection). Since you cannot retain an autorelease pool (or autorelease it—see retain and autorelease), draining a pool ultimately has the effect of deallocating it. You should always drain an autorelease pool in the same context (invocation of a method or function, or body of a loop) that it was created. See Using Autorelease Pool Blocks for more details.

Woodley answered 22/5, 2015 at 6:0 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.