views:

385

answers:

3

I'm writing a little network application in Cocoa, using objective-c 2.0. I have the garbage collector enabled in required mode (-fobjc-gc-only). When I run the code most of the time it works like a charm. But sometimes it just crashes without warning, and gdb launches. I'm as yet unable to get any useful info from GDB. The code is as follows:

    NSHost *host = [NSHost hostWithName:@"hostname"];
    [NSStream getStreamsToHost:host port:1234
                   inputStream:&iStream outputStream:&oStream];

    if (iStream == nil || oStream == nil) {
        NSLog(@"Unable to open streams.");
        return;
    }

    [iStream setDelegate:self];
    [oStream setDelegate:self];

    [iStream scheduleInRunLoop:[NSRunLoop currentRunLoop] forMode:NSDefaultRunLoopMode];
    [oStream scheduleInRunLoop:[NSRunLoop currentRunLoop] forMode:NSDefaultRunLoopMode];

    [iStream open];
    [oStream open];
}

    - (void)stream:(NSStream *)stream handleEvent:(NSStreamEvent)eventCode {
       NSLog(@"Event: '%d'", eventCode);
    }

I can execute this code a couple of times without the application crashing, but at some point it will crash. GDB gives no useful information as far as I can tell, however a stacktrace might be useful:

#0  0x9438d688 in objc_msgSend ()
#1  0x95fe3451 in _inputStreamCallbackFunc ()
#2  0x9561c549 in _CFStreamSignalEventSynch ()
#3  0x9561e117 in CFReadStreamSignalEvent ()
#4  0x90c7702f in SocketStream::socketCallback ()
#5  0x90c77153 in SocketStream::_SocketCallBack_stream ()
#6  0x956137bb in __CFSocketDoCallback ()
#7  0x95614f05 in __CFSocketPerformV0 ()
#8  0x9560a595 in CFRunLoopRunSpecific ()
#9  0x9560ac78 in CFRunLoopRunInMode ()
#10 0x90e5028c in RunCurrentEventLoopInMode ()
#11 0x90e4ffde in ReceiveNextEventCommon ()
#12 0x90e4ff19 in BlockUntilNextEventMatchingListInMode ()
#13 0x914add0d in _DPSNextEvent ()
#14 0x914ad5c0 in -[NSApplication nextEventMatchingMask:untilDate:inMode:dequeue:] ()
#15 0x914a65fb in -[NSApplication run] ()
#16 0x91473834 in NSApplicationMain ()
#17 0x00001f90 in main (argc=1, argv=0xbffff758) at /Users/blubber/Documents/MacLight/main.m:13

I can't figure out what's going wrong here. Although it seems some object (maybe either of the streams) get cleared by the GC untimely.

A: 

This is nothing but a wild guess, but try changing/adding this:

    [iStream open];
    [oStream open];
    CFRetain(self);
}

This is just for testing purposes, and you will likely 'leak' self as a result. I base this recommendation because the stack trace crashes during some kind of callback processing, and you've set yourself as the delegate, so the hunch is that the callback target is you.

If this 'fixes' the problem, then I have no idea what the problem is (strange as it sounds).

BEGIN EDIT

Wouldn't you know it, right after I posted the answer, I think I found the problem. Make the following modification (don't add the CFRetain bit):

[NSStream getStreamsToHost:host port:1234 inputStream:&iStream outputStream:&oStream];
iStream = iStream;
oStream = oStream;

IMPORTANT: both iStream and oStream must be ivars, part of your class declaration. Since the code snippet you posted leaves this open as a possibility, I just thought I'd be clear.

If its the bug I think it is, I'd be willing to bet this 'fixes' the problem too:

iStream = self;
oStream = self;
[NSStream getStreamsToHost:host port:1234 inputStream:&iStream outputStream:&oStream];

Presuming this is what I think it is, you should be able to remove the fixes, add sometihng like NSLog(@"self is: %p, iStream is: %p, oStream is: %p", self, iStream, oStream), and then wait for it to crash. Then, at the debugger, enter something like:

gdb> p *(MySelfClass *)0xdeadbeef

Where MySelfClass is your class name, and 0xdeadbeef is whatever the pointer is for self. The values for iStream and oStream should be the same as whatever NSLog spit out. Then, type in

gdb> info gc-references 0xPTR
gdb> info gc-roots 0xPTR

For each of self, iStream, oStream. I think you'll find that the only thing the GC system knows about is self, despite the fact that your ivars have pointers.

Another extremely frustrating problem I've had when using Cocoa's GC system is the compiler generates buggy code for __strong pointers (I've filed several bugs regarding this). Most of the time this doesn't cause a crash because the problem is masked by another live pointer, made worse by the fact that 99% of the time both pointers tend to 'die' at nearly the same time. The GC system only performs a collection a few times per second at most, but usually much less frequently.

Then there are the times where there isn't another pointer around to mask the compilers mistake. This is a perfect example: you need those pointers to live past multiple run loop iterations. For these cases, you're eventually, at some point, going to cross paths with a collection run. When that happens is completely random, it could be several times per second, or once a minute.

END EDIT

There is a second 'solution' to your problem: don't use GC. In fact, I strongly recommend this, and this comes from practical experience of using this GC system since 10.5 beta in real world, non-trivial programs.

One of the reasons why I discourage its use is purely pragmatic: While debugging manual memory management / reference counted problems can be hard and a waste of time, it is virtually impossible to debug GC problems. I would bet any amount of money that your problem boils down to this: It is essentially equivalent a multi-threaded race condition bug. This is the most difficult class of problems to debug.

My guess is some where, some thing has dropped the __strong qualifier to a GC managed pointer, and the biggest offender is a NSObject *object; to a void *pointer;, such as pointer = object;. The compiler lets you do this without so much as a warning even though it leads to the kinds of problems you're having and is almost always an error, or opens up the potential for these kinds of errors. Sometimes its not even obvious that you've done this, such as passing an object to an argument that takes a void * type. What's even worst is that in the later case there is nothing you can do: the method or function has not been compiled in such a way as to treat the received pointer as __strong.

There is no generic way on how to handle these cases 'correctly' so your program behaves in a deterministic fashion and doesn't crash. There literally might not even be a way. When you inevitably encounter one of these corner cases, which are much more frequent than you'd think, or cause the problem yourself by forgetting __strong, I promise you that any 'gains' in productivity that you were promised by using GC will be wiped out, to the point where not using GC would have been both much faster and much easier.

The next problem I encountered is performance problems. In the range of two to five times slower. This is because the GC system needs to be told when a GC manager pointer is stored somewhere in the heap. This is normally just a single instruction, but the compiler rewrites the assignment to a objc_assign_strongCast() function call (!) that always acquires a global GC mutex lock (!!). This is a shockingly expensive operation.

This is a pretty heretical recommendation and I'm pretty sure that this will draw out Cocoa GC apologists in droves. The arguments basically boil down to:

Apples testing showed that for real world applications there was no performance impact.

This is not a defense, and it almost always comes from an Apple employee. In no way does this claim address, negate, or help solve the performance problems of my real world app. And by academic standards, unless you're willing to provide the data and methodology that I can review and reproduce the same results, this claim can't even be taken seriously.

Lots of real world applications use GC.... like Xcode!

In all honesty, the only application I have ever seen mentioned is Xcode. One day (not that long ago) I actually went through the trouble to find all the GC using apps on my machine. There were two apps: Xcode.app and /Developer/Applications/Utilities/Property List Editor.app/. This includes whatever third party apps I had, along with the developer tools (obviously), standard OS X stuff, iLife and iWork '09. Setting aside the fact that this isn't an accurate sample of third party usage of GC, there is something very interesting that's not obvious at first: There are an awful lot of Apple applications that this covers, and there is exactly one real world app in there: Xcode.app.

This is made all the more interesting by the fact that Apple has apparently performed in house testing to determine GC impact on performance. I think it's safe to say that the apps covered would be some of the highest on the list to see if GC caused any impact to. This means that either Apple went to the effort of converting some of these apps to GC and then decided not to ship the GC version (which immediately raises the question of why not), or it raises some legitimate doubts as to just how many real world apps were used.

johne
A: 

"There is a second 'solution' to your problem: don't use GC. In fact, I strongly recommend this, and this comes from practical experience of using this GC system since 10.5 beta in real world, non-trivial programs."

I have to confirm it. I had a negative experience using GC in quite big commercial product, GC application was leaking some 180MB per hour. I could not do anything with Instruments - it was showing real "garbage", the same was GDB output. GC apps are leaking, many leeks are in Cocoa internals (I believe Apple do not use GC in most of own products, so it was not really thoroughly tested).

The other issue is performance. GC increases CPU load significantly, I experienced some 100% increase after enabling GC and I experienced "rainbow wheel" periodically for several seconds.

Now I ported application to "manual mode", get rid of leaks and got much better CPU load.

Garbage Collector is certainly a brilliant thing to have, but it's implementation is very far from being ideal in Leopard. Look forward to repeat experience in Snow Leopard, I do hope they did better job there

Anton Zemlyanov
+1  A: 

Are iStream and oStream instance variables or local variables?

An instance variable is a strong reference, so an object whose pointer you store in an ivar will stay alive (as long as your object does, anyway), but a local variable stops existing when the function exits, so an object whose pointer you store in a local variable will die with it. (The object will remain alive as long as the variable still exists and the object's pointer is still in it, but once the function exits and the variable goes away, if there's nothing else holding on to the object, the object is doomed.)

Peter Hosey