Sorry, it's a bit wordy, but I wanted to make sure I was clear! ;-)
I have an iOS app that uses FFMPEG for streaming RTSP. I've multi-threaded FFMPEG using NSOperationQueue such that most its work, other than painting the image to the screen, of course, happens in background threads.
Works great! ...except for the fact that threads the NSOperationQueue creates never die!
I init the Queue in the class' init method with:
self->opQ = [[NSOperationQueue alloc] init];
[self->opQ setMaxConcurrentOperationCount:1];
I add methods to the Queue using blocks:
[self->opQ addOperationWithBlock:^{
[self haveConnectedSuccessfullyOperation];
}];
Or
[self->opQ addOperationWithBlock:^{
if (SOME_CONDITION) {
[self performSelectorOnMainThread:#selector(DO_SOME_CRAP) withObject:nil waitUntilDone:NO];
}
}];
Later, when I need to tear down the RTSP stream, in addition to telling FFMPEG to shut down, I call:
[self->opQ cancelAllOperations];
Which does indeed stop the threads from doing any work , but never actually destroys them. Below, you'll see a screen shot of threads that are doing nothing at all. This is what my threads look like after starting/stoping FFMPEG several times.
I seem to remember reading in Apple's documentation that NSOperations and the threads they are run on are destroyed once they are done executing, unless otherwise referenced. This doesn't appear to be the case.
Do I just need to destroy the NSOperationQueue, then re-init it when I need to start up FFMPEG again (I just realized I haven't tried this)? Anyone know how I need to kill these extra threads?
THANKS!
I solved it by creating NSBlockOperations so that I could monitor the isCancelled state, while also making the new NSBlockOperations' content more intelligent, such that I simplified the routine that would add the operations to the queue.
... Plus, I made an NSOperationQueue n00b mistake: I was adding operations to the queue on a looping basis, which fired up to 30 times per second (matching the video's frame rate). Now, however, the operation is added to the queue only once and the looping behavior is contained within the operation instead of having the loop add the operation to the queue.
Previously, I had something like this (pseudo code, since I don't have the project with me):
NSTimer *frameRateTimeout = [NSTimer scheduledTimerWithTimeInterval:1/DESIRED_FRAMES_PER_SECOND target:self selector:#selector(ADD_OPERATION_TO_QUEUE_METHOD:) userInfo:nil repeats:YES];
-(void)ADD_OPERATION_TO_QUEUE_METHOD:(NSTimer *)timer {
[opQ addOperation:displayFrame];
}
Which worked well, as the OS would correctly manage the queue, but it was not very efficient, and kept those threads alive forever.
Now, it's more like:
-(id)init {
self = [super init];
if (self) {
// alloc/init operation queue
...
// alloc/init 'displayFrame'
displayFrame = [NSBlockOperation blockOperationWithBlock:^{
while (SOME_CONDITION && ![displayFrame isCancelled]) {
if (playVideo) {
// DO STUFF
[NSThread sleepForTimeInterval:FRAME_RATE];
}
else { // teardown stream
// DO STUFF
break;
}
}
}];
}
return self;
}
- (void)Some_method_called_after_getting_video_ready_to_play {
[opQ addOperation:displayFrame];
}
Thanks, Jacob Relkin, for responding to my post.
If anyone needs further clarification, let me know, and I'll post better code once I have the project in my hands again.
Related
I'm working on an iPad app that needs to spawn a dialog mid-function for some user interaction. In order to wait for the dialog, I run an NSRunLoop, however, this is preventing events on the dialog from being processed. This is how I spawn the dialog:
NSArray* listOfCompatibleTypes = [[NSArray alloc] initWithArray:[listOfCompatibleTypesAndSizesAsSet allObjects]];
[secondaryImplantChooserDialog setModalPresentationStyle:UIModalPresentationFormSheet];
[secondaryImplantChooserDialog setDefinesPresentationContext:YES];
[self presentViewController:secondaryImplantChooserDialog animated:NO completion:nil];
And the runloop is like this:
NSRunLoop* runLoop = [NSRunLoop currentRunLoop];
while (secondaryImplantChooserDialog.fDialogDone != YES)
{
[runLoop runUntilDate:[NSDate dateWithTimeIntervalSinceNow:0.01]];
}
The dialog box has a popover that contains a tableview. When I comment out the runloop, the table view's didSelectRowAtIndexPath(…) method gets called. When the loop is active, however, that method doesn't get called until the dialog gets dismissed.
I realize this isn't exactly an iOS-kosher design, so I should probably refactor it, but I was wondering if there was any way to process UI events while the runloop is active.
Polling is very rarely a good solution, particularly in a user-interactive environment like an iOS app. The functionality you are looking for is a semaphore - where you can block execution in one part of your app until another has completed. You can do this with Grand Central Dispatch but this is probably adding complexity when a better solution is to re-factor.
From what I understand you have a method which is performing some calculation or business-logic and it determines that additional information is needed. You could refactor something like:
if ([self haveEnoughInfo]) {
[self performFinalCalc];
} else {
[self gatherMoreInformation]; // Use a delegate or completion block to invoke [self performFinalCalc] once more information is gathered
}
I have an app that I'm accessing a remote website with NSURLConnection to run some code and then save out some XML files. I am then accessing those XML Files and parsing through them for information. The process works fine except that my User Interface isn't getting updated properly. I want to keep the user updated through my UILabel. I'm trying to update the text by using setBottomBarToUpdating:. It works the first time when I set it to "Processing Please Wait..."; however, in the connectionDidFinishLoading: it doesn't update. I'm thinking my NSURLConnection is running on a separate thread and my attempt with the dispatch_get_main_queue to update on the main thread isn't working. How can I alter my code to resolve this? Thanks! [If I need to include more information/code just let me know!]
myFile.m
NSLog(#"Refreshing...");
dispatch_sync( dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
[self getResponse:#"http://mylocation/path/to/file.aspx"];
});
[self setBottomBarToUpdating:#"Processing Please Wait..."];
queue = dispatch_queue_create("updateQueue", DISPATCH_QUEUE_CONCURRENT);
connectionDidFinishLoading:
if ([response rangeOfString:#"Complete"].location == NSNotFound]) {
// failed
} else {
//success
dispatch_async(dispatch_get_main_queue(),^ {
[self setBottomBarToUpdating:#"Updating Contacts..."];
});
[self updateFromXMLFile:#"http://thislocation.com/path/to/file.xml"];
dispatch_async(dispatch_get_main_queue(),^ {
[self setBottomBarToUpdating:#"Updating Emails..."];
});
[self updateFromXMLFile:#"http://thislocation.com/path/to/file2.xml"];
}
In my connectionDidFinishLoading: I would try something like this:
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT,0),^ {
if ([response rangeOfString:#"Complete"].location == NSNotFound]) {
// failed
} else {
//success
dispatch_async(dispatch_get_main_queue(),^ {
[self setBottomBarToUpdating:#"Updating Contacts..."];
});
[self updateFromXMLFile:#"http://thislocation.com/path/to/file.xml"];
dispatch_async(dispatch_get_main_queue(),^ {
[self setBottomBarToUpdating:#"Updating Emails..."];
});
[self updateFromXMLFile:#"http://thislocation.com/path/to/file2.xml"];
}
});
Then all that file access is happening in a background queue so the main queue is not locked up. The main queue will also complete this call to connectionDidFinishLoading much more quickly, since you're throwing all the hard work onto the default queue instead, which should leave it (and the main thread) ready to accept your enqueuing of the updates to the UI which will be done by the default queue as it processes the block you just enqueued to it.
The queue handover becomes
main thread callback to connectionDidFinishLoad:
rapid handoff to default global queue releasing main thread
eventual hand off to main queue for setBottomBarToUpdating: calls
performing main queue blocks on main thread to properly update UI
eventual completion of blocks on main queue
eventual completion of blocks on default queue
You've increased concurrency (good where you've good multi-core devices) and you've taken the burden of I/O off the main thread (never a good place for it) and instead got it focused on user interface work (the right place for it).
Ideally you woud run the NSURLConnection run loop off the main thread too, but this will might be enough for you to get going.
Which run loop are you running the NSURLConnection in? If it's the main loop, you're queueing up the setBottomBarToUpdating: calls behind the work you're already doing, hence the probable reason why you're not seeing the UI update.
You could also give performSelectorOnMainThread try like so:
if ([response rangeOfString:#"Complete"].location == NSNotFound]) {
// failed
} else {
//success
[self performSelectorOnMainThread:#selector(setBottomBarToUpdating) withObject:#"Updating Contacts..." waitUntilDone:false];
[self updateFromXMLFile:#"http://thislocation.com/path/to/file.xml"];
[self performSelectorOnMainThread:#selector(setBottomBarToUpdating) withObject:#"Updating Emails..." waitUntilDone:false];
[self updateFromXMLFile:#"http://thislocation.com/path/to/file2.xml"];
}
I really need help here. I'm desperate at this point.
I have NSOperation that when added to the NSOperationQueue is not being triggered.
I added some logging to see the NSOperation status and this is the result:
Queue operations count = 1
Queue isSuspended = 0
Operation isCancelled? = 0
Operation isConcurrent? = 0
Operation isFinished? = 0
Operation isExecuted? = 0
Operation isReady? = 1
Operation dependencies? = 0
The code is very simple. Nothing special.
LoadingConflictEvents_iPad *loadingEvents = [[LoadingConflictEvents_iPad alloc] initWithNibName:#"LoadingConflictEvents_iPad" bundle:[NSBundle mainBundle]];
loadingEvents.modalPresentationStyle = UIModalPresentationFormSheet;
loadingEvents.conflictOpDelegate = self;
[self presentModalViewController:loadingEvents animated:NO];
[loadingEvents release];
ConflictEventOperation *operation = [[ConflictEventOperation alloc] initWithParameters:wiLr.formNumber pWI_ID:wiLr.wi_id];
[queue addOperation:operation];
NSLog(#"Queue operations count = %d",[queue operationCount]);
NSLog(#"Queue isSuspended = %d",[queue isSuspended]);
NSLog(#"Operation isCancelled? = %d",[operation isCancelled]);
NSLog(#"Operation isConcurrent? = %d",[operation isConcurrent]);
NSLog(#"Operation isFinished? = %d",[operation isFinished]);
NSLog(#"Operation isExecuted? = %d",[operation isExecuting]);
NSLog(#"Operation isReady? = %d",[operation isReady]);
NSLog(#"Operation dependencies? = %d",[[operation dependencies] count]);
[operation release];
Now my operation do many things on the main method, but the problem is never being called. The main is never executed.
The most weird thing (believe me, I'm not crazy .. yet). If I put a break point in any NSLog line or in the creation of the operation the main method will be called and everything will work perfectly.
This have been working fine for a long time. I have been making some changes recently and apparently something screw things up. One of those changes was to upgrade the device to iOS 5.1 SDK (iPad).
To add something, I have the iPhone (iOS 5.1) version of this application that use the same NSOperation object. The difference is in the UI only, and everything works fine.
Oh, and this only fails on the actual device. In the simulator everything works ok.
Any help will be really appreciated.
Regards,
If the operation is concurrent, you need to implement -start, not -main.
Ok, I finally solve this issue.
The problem I was having was due to an NSOperation running in the background all the time (but in a different queue). This operation was blocking any other thread (NSOperation) to be executed. This was happening only in iPad 1 because is not dual core.
My NSOperation was doing something like this on the main method:
- (void)main
{
while (![self isCancelled]) {
//Do stuff
}
}
And the NSOperation was contantly doing it the whole time
Silly me, I didn't give the OS time to work with other threads, so adding a sleep made the trick
- (void)main
{
while (![self isCancelled]) {
[NSThread sleepForTimeInterval:0.5];
//Do Stuff
}
}
That sleep gives the OS chance to work with other threads.
Why putting a breakpoint was doing the work? ... the debugger stopped all the threads and because the breakpoint was in my other NSOperation, that thread was executed.
I had the same issue, but the resolution to mine was not the same, so I thought I'd throw my answer in here too for future people like me.
My problem was that in my NSOperation subclass, I had:
#property CGPoint start;
which, when synthesized, creates the method:
-(CGPoint)start
which overrides NSOperation's -(void)start; method, which is the signifier for a non-concurrent NSOperation, and was preventing all the regular stuff that goes on in -(void)start from happening, which thus prevents the -(void)main method from being called at all.
Once I renamed my property to something other than start, it worked fine.
Ok, this issue is only happening with iPad 1 devices with application compiled with the SDK 5.1.
I tried with an iPad 1 with iOS 4.2.1 and iPad 1 with iOS 5.1. Both gives the same problems.
For sure this was working in both iPads with application compiled with SDK 4.3
The problem is that I manage scrollView with lots of tiles in it.
Each visible tile display image loaded from URL or (after first URL load) cached file in background. Invisible tiles recycles (set new frame and redraw).
Image load depends on tile position.
With long range scroll there is multiple redraw called for each tile: each tile loads (and display) different image several times before display the correct one.
So problem is to cancel all previously added operations for tile before add new.
I subclass NSInvocationOperation just to contain context object to detect operation attached to and before add new operation I canceling all operation for same tile:
-(void)loadWithColler:(TileView *)coller {
if (queue == nil) {
queue = [NSOperationQueue new];
}
NSInvocationOperationWithContext *loadImageOp = [NSInvocationOperationWithContext alloc];
[loadImageOp initWithTarget:self selector:#selector(loadImage:) object:loadImageOp];
[loadImageOp setContext:coller];
[queue setSuspended:YES];
NSArray *opers = [queue operations];
for (NSInvocationOperationWithContext *nextOperation in opers) {
if ([nextOperation context] == coller) {
[nextOperation cancel];
}
}
[queue addOperation:loadImageOp];
[queue setSuspended:NO];
[loadImageOp release];
}
And in operation itself I check isCancelled:
-(void)loadImage:(NSInvocationOperationWithContext *)operation {
if (operation.isCancelled) return;
TileView *coller = [operation context];
/* TRY TO GET FILE FROM CACHE */
if (operation.isCancelled) return;
if (data) {
/* INIT WITH DATA IF LOADED */
} else {
/* LOAD FILE FROM URL AND CACHE IT */
}
if (operation.isCancelled) return;
NSInvocationOperation *setImageOp = [[NSInvocationOperation alloc] initWithTarget:coller selector:#selector(setImage:) object:cachedImage];
[[NSOperationQueue mainQueue] addOperation:setImageOp];
[setImageOp release];
}
But it is do nothing. Some times early returns works but tiles still load many images before the correct one.
So how could I success? And could this lots of unneeded operations cause delays on main thread when scrolling? (Because delays are exists and I do not know why...all load in background..)
Update:
With NSLog:
isCancelled while executing:
>
cancel loadImage method for:
>
So canceling work.
Now I save reference to last operation in TileView object and perform setImage operation only if invoked operation is equal to TileView operation.
Doesn't make any difference...
Looks like there IS number of operations to load different images to one tile invoked one after another.
Any another suggestions?
For clearance:
There is singleton DataLoader (all code from it). And all tiles has call to it in drowRect:
[[DataLoader sharedDataLoader] loadWithColler:self];
Update:
NSInvocationOperation subclass:
#interface NSInvocationOperationWithContext : NSInvocationOperation {
id context;
}
#property (nonatomic,retain,readwrite) id context;
#end
#implementation NSInvocationOperationWithContext
#synthesize context;
- (void)dealloc
{
[context release];
[super dealloc];
}
#end
Thanks a lot for any help!
SOLUTION:
From answer below: need to subclass from NSOperation
As I subclass NSOperation and put all loadImage: code into it "main" method (just move all code here and nothing else) and all work just perfect!
As about scroll delaying: it occurs cause loading images to UIImageView (it takes long time because of decompress and rasterize (as I understood).
So better way is to use CATiledLayer. It loads data in background and do it much faster.
The delays on main thread is due to a the mode of the runloop while you scroll. I suggest you to watch the WWDC2011 networking app sessions. I don't know if it is fine to subclass an NSInvocationOperation that is a concrete subclass of NSOperation. I will subclass NSOperation instead. For my experience if you like to avoid sluggish scrolling, you should create NSOperation subclasses that load their main on a specific thread for networking operation (you must create it). There is a wonderful sample code from apple https://developer.apple.com/library/ios/#samplecode/MVCNetworking/Introduction/Intro.html
The way that NSOperationQueue works with respect to "setSuspended" is that it won't start to run newly added NSOperations added to it after that point, and won't start to run any that are currently in it that haven't started running yet. Are you sure your operations you're trying to cancel haven't already started yet?
Also - does your NSOperation subclass correctly deal with Key Value Observing? Concurrent Queue subclassed NSOperations have to call willChangeValueForKey and didChangeValueForKey for some properties here - but doesn't look like that's the issue as your queue doesn't set isConcurrent. Just FYI if you go that route.
For example in a thread (because I can't wait in main loop) I have this code :
-(void) game {
for (Players player in players) {
if (player.type == IA) { // computer plays
answer = [player play];
else {
[ui showQuestion]; // user plays with the touch screen
// here waiting for the answer touch
answer = ???????????????? // return from waiting after touch callback
}
[answersArray addObject:answer];
}
answer = [self bestAnswer : answersArray];
[ui showTheBestAnswer : answer];
}
Is there a solution to wait for an UI Event in a fixed code place ?
without blocking the main loop of course.
Thank you very much for your help,
jpdms
First of all, I highly recommend that you read Apple's Concurrency Programming Guide, included with the Xcode documentation. In general, there are much better alternatives to threads, especially in the example you provide.
However:
If the game method is executing on a separate thread, then the proper way to signal the thread is using NSCondition. Create an instance and make sure both the code above and the touch handler has access to it.
NSCondition *playerDidTouchCondition = [[NSCondition alloc] init];
In the game method, you wait on the condition like this:
[ui showQuestion];
[playerDidTouchCondition lock];
[playerDidTouchCondition wait];
[playerDidTouchCondition unlock];
// do something with answer
Your game thread will sleep until the condition has been signaled. In your touch handler you would do this:
answer = whatever the user did
[playerDidTouchCondition lock];
[playerDidTouchCondition signal]; // wake up one of the sleeping threads
[playerDidTouchCondition unlock];
The example code you have above really doesn't demonstrate the need for a separate thread, however. You could very easily store a currentPlayerIndex somewhere and proceed to the next player inside of the button handler for the answer button.
Also, you MUST ensure that any UI updates are actually happening on the main thread. I hope that your lines like [ui showQuestion] are queuing calls on the main thread. In Cocoa you can do this easily with something like: [ui performSelectorOnMainThread:#selector(showQuestion)];
You really, really, really should not be using a separate thread for this.