Reputation: 13
I'm currently trying make a queueHandler
that takes an object array as input for executing drive commands on a simple Double robot. I'm currently trying to use GCD in order to serially execute my functions, but when I'm using dispatch_sync
on my queue in won't wait until the NSTimer
has run its course, but will continue to try and execute the commands from the next object in my array.
I have 3 functions, one which simply initializes an NSMutableArray
(loadCommands) with 2 objects and runs the queueHandler
, this is called when I toggle a switch. Then the queueHandler
reads the variables from the objects(type, timing, queueNr) to determine what type of drive function will be executed and for how long. This I thought could be done in a switch statement, and I figured it would be great if the app could execute the function on the main thread(that is ok!) but it should wait until the NSTimer
has run its course. I thought encapsulating the switch case with a dispatch_sync would solve this but it promptly skips to the next iteration in the loop and tries to execute the next function instead, which is drive backwards for 3 seconds.
When I test this with a single object in the array the command will be executed without trouble. I suppose I'm locking up the main thread somehow. Would perhaps waiting for a return value from the function in the @selector
in the NSTimer
statement help?
I've only played with Objective C for about 10 days, I'd appreciate any help I could get with this bit!
- (void)loadCommands {
//create an objectArray and put 2 objects inside it.
NSMutableArray *driveCommandsArray = [[NSMutableArray alloc] initWithCapacity:4];
//Command 1
DRCommands *C1 = [[DRCommands alloc] init];
C1.timing = 3;
C1.type = 1;
C1.queueNr = 1;
[driveCommandsArray addObject:C1];
//Command 2
DRCommands *C2 = [[DRCommands alloc] init];
C2.timing = 3;
C2.type = 2;
C2.queueNr = 2;
[driveCommandsArray addObject:C2];
//call queueHandler
[self queueHandler:driveCommandsArray];
}
Queue handler:
- (void)queueHandler: (NSMutableArray*) commandArray {
//Now, I'm not sure what I'm doing here, I watched a tutorial that
//solved a vaguely similar problem and he put a dispatch_async before the
//dispatch_sync. I can't run the dispatch_sync clause inside the case
//statement without this.
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
NSLog(@"Inside handler!");
unsigned long count;
count = [commandArray count]; //retrieve length/number of objects from the array.
unsigned long a;
for (a = 0; a < count;) {
//run the loop until all objects has been managed.
DRCommands* myObj = (DRCommands*)[commandArray objectAtIndex:a];
//create 2 serial queues.
dispatch_queue_t myQ1;
myQ1 = dispatch_queue_create("myQ1", NULL);
dispatch_queue_t myQ2;
myQ2 = dispatch_queue_create("myQ2", NULL);
int queueID = myObj.queueNr; //retrieve place in queue (not really used yet)
int timeID = myObj.timing; //retrieve the amount of time the command shall be run through the NSTimer
int typeID = myObj.type; //type of command
NSLog(@"Inside for loop!");
if (queueID == a+1) {
a++;
switch (typeID) {
{
case 1:
NSLog(@"inside case 1");
dispatch_sync(myQ1, ^{ //doesn't wait for NSTimer to finish,
//letting the Double drive forward for 3 seconds,
//before resuming operations.
counter_ = timeID;
seconds.text = [NSString stringWithFormat:@"%d", counter_];
timer = [NSTimer scheduledTimerWithTimeInterval:1 target:self selector:@selector(jDriveForward) userInfo:nil repeats:YES];
});
break;
}
{
case 2:
NSLog(@"inside case 2");
dispatch_sync(myQ2, ^{
counter_ = timeID;
seconds.text = [NSString stringWithFormat:@"%d", counter_];
timer = [NSTimer scheduledTimerWithTimeInterval:1 target:self selector:@selector(jDriveBackward) userInfo:nil repeats:YES];
});
break;
}
//add more cases
{
default:
break;
}
}
}
NSLog(@"Exited for loop, and count is %lu", a);
}
});
}
Drive commands:
//Go forward X seconds.
- (void)jDriveForward {
shouldDriveForward_ = YES; //sets a condition which is recognized by a callback function to run the device forward.
counter_ -= 1;
seconds.text = [NSString stringWithFormat:@"%d", counter_];
if (counter_ <= 0) {
[timer invalidate];
shouldDriveForward_ = NO;
}
}
//Go backwards X seconds.
- (void)jDriveBackward {
shouldDriveBackward_ = YES;
counter_ -= 1;
seconds.text = [NSString stringWithFormat:@"%d", counter_];
if (counter_ <= 0) {
[timer invalidate];
shouldDriveBackward_ = NO;
}
}
Provided drive function from the experimental API I'm using
I'm using a "token" such as "shouldDriveForward_" inside the function driveDoubleShouldUpdate which is TRUE for the duration of an NSTimer. I must call my drive methods inside that function for the robot not to default to idle mode. So whenever it is true for X duration, the function for driving forwards or backwards is active.
- (void)doubleDriveShouldUpdate:(DRDouble *)theDouble {
float drive = (driveForwardButton.highlighted) ? kDRDriveDirectionForward : ((driveBackwardButton.highlighted) ? kDRDriveDirectionBackward : kDRDriveDirectionStop);
float turn = (driveRightButton.highlighted) ? 1.0 : ((driveLeftButton.highlighted) ? -1.0 : 0.0);
[theDouble drive:drive turn:turn];
//below are custom functions
//The NSTimer I'm using keep the BOOL values below TRUE for X seconds,
//making the robot go forward/backward through this callback
//method, which I must use
if(shouldDriveForward_ == YES) {
[theDouble variableDrive:(float)1.0 turn:(float)0.0];
}
if(shouldDriveBackward_ == YES) {
[theDouble variableDrive:(float)-1.0 turn:(float)0.0];
}
}
Upvotes: 1
Views: 701
Reputation: 29946
You're kind of jumbled up here with the combination of GCD and NSTimer
. There's nothing to say they can't be intermixed, but an all-GCD approach might be easier to get your head around. I think I've discerned the gist of what you're trying to do here, and hacked something together that might be helpful. I've put the whole project up on GitHub, but here's the meat of it:
#import "ViewController.h"
typedef NS_ENUM(NSUInteger, DRCommandType) {
DRCommandUnknown = 0,
DRCommandTypeForward = 1,
DRCommandTypeBackward = 2,
};
@interface DRCommand : NSObject
@property DRCommandType type;
@property NSTimeInterval duration;
@end
@implementation DRCommand
@end
@interface ViewController ()
@property (weak, nonatomic) IBOutlet UILabel *commandNameLabel;
@property (weak, nonatomic) IBOutlet UILabel *secondsRemainingLabel;
@property (strong, atomic) DRCommand* currentlyExecutingCommand;
@property (copy, atomic) NSNumber* currentCommandStarted;
@end
@implementation ViewController
- (void)viewDidLoad
{
[super viewDidLoad];
// Do an initial UI update
[self updateUI];
}
- (IBAction)loadCommands:(id)sender
{
DRCommand *C1 = [[DRCommand alloc] init];
C1.duration = 3.0;
C1.type = DRCommandTypeForward;
DRCommand *C2 = [[DRCommand alloc] init];
C2.duration = 3.0;
C2.type = DRCommandTypeBackward;
[self handleCommands: @[ C1, C2 ]];
}
- (void)handleCommands: (NSArray*)commands
{
// For safety... it could be a mutable array that the caller could continue to mutate
commands = [commands copy];
// This queue will do all our actual work
dispatch_queue_t execQueue = dispatch_queue_create(NULL, DISPATCH_QUEUE_SERIAL);
// We'll target the main queue because it simplifies the updating of the UI
dispatch_set_target_queue(execQueue, dispatch_get_main_queue());
// We'll use this queue to serve commands one at a time...
dispatch_queue_t latchQueue = dispatch_queue_create(NULL, DISPATCH_QUEUE_SERIAL);
// Have it target the execQueue; Not strictly necessary but codifies the relationship
dispatch_set_target_queue(latchQueue, execQueue);
// This timer will update our UI at 60FPS give or take, on the main thread.
dispatch_source_t timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, dispatch_get_main_queue());
dispatch_source_set_timer(timer, DISPATCH_TIME_NOW, (1.0/60.0) * NSEC_PER_SEC, (1.0/30.0) * NSEC_PER_SEC);
dispatch_source_set_event_handler(timer, ^{ [self updateUI]; });
// Suspend the latch queue until we're ready to go
dispatch_suspend(latchQueue);
// The first thing to do for this command stream is to start UI updates
dispatch_async(latchQueue, ^{ dispatch_resume(timer); });
// Next enqueue each command in the array
for (DRCommand* cmd in commands)
{
dispatch_async(latchQueue, ^{
// Stop the queue from processing other commands.
dispatch_suspend(latchQueue);
// Update the "machine state"
self.currentlyExecutingCommand = cmd;
self.currentCommandStarted = @([NSDate timeIntervalSinceReferenceDate]);
// Set up the event that'll mark the end of the command.
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(cmd.duration * NSEC_PER_SEC)), execQueue, ^{
// Clear out the machine state for the next command
self.currentlyExecutingCommand = nil;
self.currentCommandStarted = nil;
// Resume the latch queue so that the next command starts
dispatch_resume(latchQueue);
});
});
}
// After all the commands have finished, add a cleanup block to stop the timer, and
// make sure the UI doesn't have stale text in it.
dispatch_async(latchQueue, ^{
dispatch_source_cancel(timer);
[self updateUI];
});
// Everything is queued up, so start the command queue
dispatch_resume(latchQueue);
}
- (void)updateUI
{
// Make sure we only ever update the UI on the main thread.
if (![NSThread isMainThread])
{
dispatch_async(dispatch_get_main_queue(), ^{ [self updateUI]; });
return;
}
DRCommand* currentCmd = self.currentlyExecutingCommand;
switch (currentCmd.type)
{
case DRCommandUnknown:
self.commandNameLabel.text = @"None";
break;
case DRCommandTypeForward:
self.commandNameLabel.text = @"Forward";
break;
case DRCommandTypeBackward:
self.commandNameLabel.text = @"Backward";
break;
}
NSNumber* startTime = self.currentCommandStarted;
if (!startTime || !currentCmd)
{
self.secondsRemainingLabel.text = @"";
}
else
{
const NSTimeInterval startTimeDbl = startTime.doubleValue;
const NSTimeInterval currentTime = [NSDate timeIntervalSinceReferenceDate];
const NSTimeInterval duration = currentCmd.duration;
const NSTimeInterval remaining = MAX(0, startTimeDbl + duration - currentTime);
self.secondsRemainingLabel.text = [NSString stringWithFormat: @"%1.3g", remaining];
}
}
@end
Let me know in a comment if there's any part you'd like more explanation on.
Note: The other answer here has the command doing a sleep
; my approach is fully asynchronous. Which approach is right for you will depend on what your commands are actually doing which wasn't clear from the question.
Upvotes: 1
Reputation: 115041
You only need a single serial dispatch queue to which you will add your tasks.
I would start by defining a task class which implements your various commands - you can subclass if required.
DRCommand.h
#import <Foundation/Foundation.h>
@interface DRCommand : NSObject
@property uint duration;
-(void) dispatch;
@end
DRCommand.m
#import "DRCommand.h"
@implementation DRCommand
-(void)dispatch {
[self startCommand];
sleep(self.duration);
[self stopCommand];
}
-(void) startCommand {
NSLog(@"Override this method to actually do something");
}
-(void) stopCommand {
NSLog(@"Override this method to stop doing something");
}
@end
Then your run queue code will be something like
-(void) runQueue {
DRCommand *c1=[DRCommand new];
c1.duration=5;
DRCommand *c2=[DRCommand new];
c2.duration=7;
DRCommand *c3=[DRCommand new];
c3.duration=3;
NSArray *taskArray=@[c1,c2,c3];
dispatch_queue_t queue;
queue = dispatch_queue_create("com.example.MyQueue", NULL);
for (DRCommand *command in taskArray) {
dispatch_async(queue, ^{
[command dispatch];
});
}
}
Note that you would have subclasses of DRCommand
such as DRForwardCommand
, DRBackwardCommand
and so on, each with appropriate startCommand
and stopCommand
methods.
Upvotes: 0