3

I am writing code to render and rotate a picture whose details are being simultaneously calculated and updated. It works error-free on a single thread (with a display link), but looks clunky, and I don't want the calculations to be triggered by the display link. So I want to execute all the OpenGL-related code in the main thread (with the display link) and all the calculations in a second thread (executing a while (YES) loop).

I implemented this using NSThread. It works beautifully for a while and then fails with 'Thread 1: Program received signal: "EXC_BAD_ACCESS"' during glDrawArrays, and sometimes has weird flashes of graphics. This is what I expected if the main thread reads the model-level data at the same time the second thread was overwriting it.

I then defined an NSLock in the model object and locked it for all writing (in my model class) and reading (in my view class)... but it can still result in the same error, and the graphics still occasionally has weird flashes.

Have I done something wrong here, or is my problem somewhere else?

Secondly, what is the right way to stop the second thread in this case? The NSThread class reference suggests using cancel, checking isCancelled, and exiting if so, but it also says invoking exit should be avoided.

Here are the modifications to the code - in my controller class (I'm using XCode 4.2 with ARC; all my ivars are nonatomic):

@interface MyController : NSObject {
    NSThread *calcThread;
    ...
}
// (I do not give it an @property or @synthesize line)

@implementation MyController
- (void) awakeFromNib {
    calcThread = [[NSThread alloc] initWithTarget:self 
            selector:@selector(calcLoop:) object:nil];
    [calcThread start];
    ...
}
- (void) calcLoop:(id)arg { 
    @autoreleasepool {
        while (YES)
            [myModel calculate];
    }
}
...

I put the NSLock in my model class:

@interface MyModel : NSObject {
    NSLock* oLock;
    ...
}
@property (nonatomic, strong) NSLock* oLock;

@implementation myModel
-(id) init {
    oLock = [[NSLock alloc] init];
    ...
}
-(void) changeModelAppearance {
    [oLock lock];
    ...
    [oLock unlock];
}
...

and in my view class:

@implementation MyView
-(void) modelUpdated:(NSNotification *) notification {
// modelUpdated is called via the NSNotificationCenter
    MyModel* myModel = (MyModel*) [notification object];
    [myModel.oLock lock];
    ... // update OpenGL structures with data from myModel
    [myModel.oLock unlock];
}
...

Thanks!

Racing Tadpole
  • 4,270
  • 6
  • 37
  • 56
  • Where is your call to `glDrawArrays`? – 一二三 Dec 27 '11 at 11:10
  • The call to `glDrawArrays` is in the view class, in another method (`render`); it does not refer to the model at all. – Racing Tadpole Dec 27 '11 at 21:25
  • I realised one problem with my code is that the `modelUpdated` method could be updating the OpenGL structures at the same time the `render` method is called, since NSNotificationCentre can call it from any thread. So I've now inserted a second lock in modelUpdated, and also put it in the render method around `glDrawArrays`. But I still get the same error. – Racing Tadpole Jan 02 '12 at 05:47
  • Is it possible that the OpenGL structures set in `modelUpdated:` are being modified/freed before `render` is called? – 一二三 Jan 02 '12 at 06:10
  • thanks - no, I don't think so. They're only changed in modelUpdated (or if the view is released, which shouldn't be happening). One thought I had in case it's relevant - my model class has lots of ivars, including one mutable array of another model class, which itself has lots of ivars. The coords and colour are taken from some of these last ivars. I haven't used `NSLock` when the other ivars in either model class are read or written to. (I do use `NSLock` if the mutable array is added to.) – Racing Tadpole Jan 02 '12 at 08:26
  • First, I'd advise finding a new way to sync threads. Better yet, avoid having to sync them. Perhaps you can break model updates into those that affect visible state versus those that don't. Do the latter on your background thread. Do the former in between frame renders. If there is some overlap, use a double-buffering technique (two copies, one for writing, one for reading). Reduce the overlap to as small as you can make it, to save memory. Also, to exit a thread, just do "while(!cancel)". When the last code finishes on a thread, the thread should go away. – Bored Astronaut Jan 05 '12 at 20:04
  • I can easily break the model updates into two groups as you suggest, and using your approach I think I can get away with just one NSLock object instead of two. But I don't see how to reduce it to none - even with double-buffering, I'll still need to update the "reading" model, and at that point won't I need an NSLock in case the "writing" model is being written to at the same time? Thanks for your help, much appreciated! – Racing Tadpole Jan 11 '12 at 04:20
  • I followed this through - I removed `modelUpdated:` - in fact the whole `NSNotification` structure I had built - and now check at the top of `render` whether there has been an update. If there has, I do what used to be in `modelUpdated:`. It means my view class now has a reference to the model object, which I was trying to avoid. But it means I can get rid of the two interlocking `NSLock`s, and the whole thing works. So thanks! – Racing Tadpole Jan 22 '12 at 10:57

1 Answers1

3

I think you would have a much easier time using grand central dispatch in this case.

@interface MyController : NSObject { // Not use why you're not inheriting from NSController here.
    dispatch_queue_t calQueue;
    ...
}

- (void) awakeFromNib {
    calcQueue = dispatch_queue_create("com.yourApp.calc", DISPATCH_QUEUE_SERIAL);
    dispatch_async(calcQueue, ^{
        while(YES) // This will peg the CPU at 100%
            [myModel calculate];
    });
}

model class

@interface MyModel : NSObject {
    dispatch_queue_t modelQueue;
    ...
}
@property dispatch_queue_t modelQueue;

@implementation myModel
-(id) init {
    modelQueue = dispatch_queue_create("com.yourApp.model", DISPATCH_QUEUE_SERIAL);
}

-(void) dealloc {
    dispatch_release(modelQueue);
}

-(void) changeModelAppearance {
    dispatch_async(modelQueue, ^{
        ...
    });
}
...

View

@implementation MyView
-(void) modelUpdated:(NSNotification *) notification {
// modelUpdated is called via the NSNotificationCenter
    MyModel* myModel = (MyModel*) [notification object];
    dispatch_async(model.modelQueue, ^{
        ... // update OpenGL structures with data from myModel 
    });
}
...

To pause any of the Queues you just call dispatch_suspend and to restart any queue use dispatch_resume

If you use a timer instead of the an infinite loop you can reduce the amount of CPU you are using.

calcTimer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0));
dispatch_source_set_timer(calcTimer, DISPATCH_TIME_NOW, DT, 1000);
dispatch_source_set_event_handler(calcTimer, ^{
    ...
});
dispatch_resume(calcTimer);

This would use much lower cpu overhead.

user1139069
  • 1,505
  • 2
  • 15
  • 27
  • Thanks - I really appreciate your help - I was wondering if GCD would help me. Your first almost parenthetical comment "not sure why you're not inheriting from NSController" made me realise how much I have to learn about Cocoa... and after reading more, has me thinking about refactoring to use KVO and in particular the Receptionist pattern, which seems suited to this problem. Thanks for showing me the light! Apple's description of the Receptionist pattern in its Cocoa Fundamentals Guide includes some code for executing across threads too, using `NSOperationQueue` ... – Racing Tadpole Jan 22 '12 at 11:14