4

I understand how Automatic Reference Counting works:

At compile time, it is determined the possible types of relationships between objects and thus where releases may occur, then at run time, the number of strong pointer references to each object is tracked and it is released when that number reaches 0. I have yet to encounter problems with this in concept or in practice, at least when dealing with the main thread.

I noticed that when I start a new background thread, it does not release any produced objects until the thread ends. I ran an example:

enter image description here

Essentially, an automatic '@autoreleasepool' is placed around the thread call, so it should be obvious that placing my own will not fix this issue. In fact, I have tested that with the same results. If I am correct in that, then the existence of that enforced pool is exactly causing my issue, but I assume that is the best that ARC can perform on a multithreaded application. The memory usage slowly and consistently inclines. If I leave this thread too long, the app eventually runs out of memory. This is a problem since the thread needs to be able to run indefinitely at worst.

I removed some of the main allocing in the thread already. I believe that I have determined that some of the remaining memory allocing are NSNumbers being released from an NSMutableArray because I am overwriting to it.

So I suppose I will have to do one of the following:

  1. Remove consistent allocing in the thread altogether.
  2. Change app to non-ARC to manually release memory in background thread.
  3. Detect when memory is high, save the state of the thread, sync with main thread to release objects, then resume the algorithm.
  4. Find out if there exists some way to notify the main thread or ARC that I want an object synchronized so that it may be released.
  5. Realize that Apple actually has a way to dispatch ARC to properly handle another threads asynchronously and never said anything about it on the main reference page.
  6. Disable ARC in the files with the alloced objects such as the dictionary. How can I disable ARC for a single file in a project?

None of those look like pretty solutions to my problem, although I may try 1 or 6. Does anyone have advice?

Update:

I ran the same algorithm but with the following autorelease blocks added to the code, at first thinking I was going to disprove rmaddy and Aaron Brager's responses.

-(void)setInt: (int)value For: (NSString *)variableName {
    @autoreleasepool {
        [self.intDictionary setValue:@(value) forKey:variableName];
    }
}

-(void)setBool: (bool)value For: (NSString *)variableName {
    @autoreleasepool {
        [self.boolDictionary setValue:@(value) forKey:variableName];
    }
}

Here is the resulting memory allocation graph:

enter image description here

They were correct. I am glad that I was wrong in this case. It means my coding is going to be a lot easier than I was starting to imagine.

Community
  • 1
  • 1
Timothy Swan
  • 623
  • 3
  • 9
  • 21
  • I suggest using the zombies profiler in instruments to determine what is not being released. Also, it would help (me) if you provide some of your code that relates to your implementation of a background thread. – Justin Moser Aug 03 '14 at 03:06
  • 4
    None of this has anything to do with threads really. You just happen to create a lot of autoreleased objects on your background thread. Simply add additional uses of `@auoreleasepool` where applicable. – rmaddy Aug 03 '14 at 03:08
  • @rmaddy ... it also seems to be little to do with ARC. – Tommy Aug 03 '14 at 03:10
  • @rmaddy Sorry, but that won't work. The entire thread dispatch is in a pool already. Adding nested pools won't override that. Even if they could, I need to dynamically release the extra objects from the dictionary, I don't create and release them in the same block without leaving that block non-deterministic number of times first. – Timothy Swan Aug 03 '14 at 03:13
  • @rmaddy Ah, you actually were right, maddy. Apparently, the inner calls are favored and even if something was alloced earlier, it may be released at the end of the block. Refer to the image update. – Timothy Swan Aug 03 '14 at 03:26
  • It might be overkill to add `@autoreleasepool` at the level you show in the update. There may be better places to do so. But it depends on your code and what you are doing. – rmaddy Aug 03 '14 at 04:09

1 Answers1

5

Autoreleased objects are deallocated when their autorelease pool drains. This typically occurs at the end of a thread's run loop.

This behavior is the same whether you're talking about the main thread or a background thread. And of course, it only applies to objects that no longer have any strong references.

This analysis of yours is not entirely correct:

Essentially, an automatic '@autoreleasepool' is placed around the thread call, so it should be obvious that placing my own will not fix this issue.

Consider this code:

@autoreleasepool {
    for (int i = 0; i < 100000; i++) {
        // create an expensive autoreleased object
        // do something with it
    }
}

In this case, none of the autoreleased objects will be deallocated until the end of the run loop when the autorelease pool drains.

However, if you add your own autorelease pool:

@autoreleasepool {
    for (int i = 0; i < 100000; i++) {
        @autoreleasepool {
            // create an expensive autoreleased object
            // do something with it
        }
    }
}

The objects will be deallocated when the inner pool drains, for each iteration of the for loop.

If adding your own autorelease pool as shown above didn't resolve your issue, then the remaining possibilities are:

  1. You are using ARC and the objects are not getting deallocated because they still have a strong reference (perhaps you have a strong reference cycle)
  2. Your increase in memory usage is not from Objective-C objects (for example, you created a CGImageRef and never called CGImageRelease)
  3. You are mistakenly not using ARC for this class (and you're not calling release)

Your six proposed solutions could also resolve the problem, but it's probably easier to fix than that.

Aaron Brager
  • 65,323
  • 19
  • 161
  • 287
  • Is '@autoreleasepool' the only place for memory to be released? I'm curious how my memory was able to drop after the extra thread ended because the only '@autoreleasepool' in my application is the one wrapping the appdelegate. Are there auto generated ones for TableViewControllers? – Timothy Swan Aug 03 '14 at 03:17
  • The autorelease pool in your app delegate is for your main thread (which is where a table view controller runs, generally). If you are using Grand Central Dispatch (`dispatch_async…`) then auto release pools are created for you but [you're encouraged to add your own](http://stackoverflow.com/a/4141454/1445366). – Aaron Brager Aug 03 '14 at 03:22
  • Ahh, I see. However, I am now confused about what may just be a minor issue. Do you notice that my autoreleasepool blocks are only around a place where I am releasing memory and not where I am allocing it? Why then, is it even a block? Why isn't it just some function call if it only happens at the end of the block anyway? Is that to prevent someone from having a return statement inside the block or something, just to enforce reaching the actual release? – Timothy Swan Aug 03 '14 at 03:43
  • The block needs a beginning since this is when the autorelease pool is created. (Autoreleased objects are added to this pool). – Aaron Brager Aug 03 '14 at 04:07
  • You're saying that every object which appears within the brackets (unless perhaps they are within deeper brackets) are tracked then at the end of the block those and only those are considered for release? – Timothy Swan Aug 03 '14 at 08:14