Re: Cocoa idiom for time consuming tasks
Re: Cocoa idiom for time consuming tasks
- Subject: Re: Cocoa idiom for time consuming tasks
- From: Mike Davis <email@hidden>
- Date: Tue, 5 Feb 2002 00:22:24 -0500
On Monday, February 4, 2002, at 11:05 PM, Joe Chan wrote:
Hi Mike,
Do you mean by the Command pattern as described in the GoF book? I
would imagine you just stuff NSInvocation into the queue as commands,
right? What do you mean by cancelable thread?
Yeah, the command pattern in Design Patterns. The way I implemented it
was a queue using NSArray and a mutex using NSConditionLock.
A cancelable thread is one which can be terminated gracefully by a
different thread. The cancel part refers to blocking calls, such as
socket I/O of blocking on a mutex or semaphore.
To implement it "fully", create a thread class (probably your consumer)
which can have abstract "BlockingCall" classes (a protocol would be okay
with ObjC) associated with it. When you need to block the thread,
register the blocking call with the thread object so that when you
cancel the thread it knows which object is blocking. The blocking
interface has a "cancel" method which, in the case of a socket, would
just close the socket making the blocking socket call fail. The code
after the blocking calls itself has to check to see if it's supposed to
be canceled, rather than an error, and if so throws an exception. Use
the exception to cleanup and to unwind back to the main thread method,
so you can exit the thread.
You will also need to deal with re-entrant issues for the NSConnection
from your command to the main application thread. I dealt with it by
having the cancel method kill the NSConnection. Remember the cancel is
called from the main thread so the command will die because I have a
timeout on the reply for the shared connection. You get an exception at
that point.
In my app, I'm building an index to a few thousand files. So ideally,
the user can pause or cancel the operation in the thread at any time.
In addition, the indexing code will feedback some progress update to
the main thread. I already have the NSConnection stuff working: the
progress method is a oneway method, so the thread doesn't have to block
on updates. For canceling, I'm thinking of just have the thread check a
queue for a cancel command.
I clear the queue and issue a "cancel" to the command so the thread
returns. I then wait until the thread terminates.
As for pausing, I don't really know exactly what to do. A wait command
is really not what I want: once the thread get a wait command, it
basically has to busy wait (or sleep a little, then check the queue
again). I'm thinking of using an NSLock that is shared between the
threads. Every time around the loop, the indexing thread will try to
acquire and then release the lock immediately, before checking the
command queue. That way, if the user clicks the pause button, the main
thread will spin until it can acquire the lock, causing the indexing
thread to block. It is somewhat of a hack; I wish I could suspend a
thread in Cocoa, it would've solved the whole problem rather easily.
I think you can put the thread to sleep with p-thread calls. I've not
tried it myself though. It sounds more like you want a
WaitOnMultipleObjects(), an NT call which is very useful. You could
emulate it with a select() since select() watches for semaphores,
effectively.
Here's the code I have for the command queue...
//
// CommandQueue.h
// Only Mortal
//
// Created by mdavis on Thu Oct 11 2001.
// Copyright (c) 2001 __MyCompanyName__. All rights reserved.
//
#import <Foundation/Foundation.h>
@protocol CommandProtocol
- (void)execute;
- (void)cancel;
@end
@protocol CommandObserver
- (void)update:(id)sender;
@end
@interface CommandQueue : NSObject {
NSMutableArray *queue;
NSConditionLock *lock;
NSConditionLock *threadTerminated;
id<CommandProtocol> currentCommand;
NSMutableArray *observers;
}
- (void)add:(id<CommandProtocol>)command;
- (void)removeAllCommands;
- (void)terminate;
- (BOOL)itemsToBeProcessed;
- (void)addObserver:(id<CommandObserver>)observer;
- (void)removeObserver:(id<CommandObserver>)observer;
@end
//
// CommandQueue.m
// Only Mortal
//
// Created by mdavis on Thu Oct 11 2001.
// Copyright (c) 2001 __MyCompanyName__. All rights reserved.
//
#import "CommandQueue.h"
#define NO_COMMAND 0
#define COMMAND_AVAILABLE 1
#define THREAD_RUNNING 0
#define THREAD_SHOULD_STOP 1
#define THREAD_STOPPED 2
@interface CommandQueue(Private)
- (void)run:(id)object;
- (id<CommandProtocol>)next;
@end
@implementation CommandQueue
- init
{
[super init];
queue = [[NSMutableArray alloc] init];
lock = [[NSConditionLock alloc] initWithCondition:NO_COMMAND];
threadTerminated = [[NSConditionLock alloc]
initWithCondition:THREAD_RUNNING];
observers = [[NSMutableArray alloc] init];
[NSThread detachNewThreadSelector:@selector(run:) toTarget:self
withObject:nil];
return self;
}
- (void)dealloc
{
[threadTerminated release];
[lock release];
[queue release];
[observers release];
[super dealloc];
}
- (void)addObserver:(id<CommandObserver>)observer
{
[observers addObject:observer];
}
- (void)removeObserver:(id<CommandObserver>)observer
{
[observers removeObject:observer];
}
- (void)add:(id<CommandProtocol>)command
{
[lock lock];
[queue addObject:command];
[lock unlockWithCondition:COMMAND_AVAILABLE];
}
- (void)removeAllCommands
{
[lock lock];
if( currentCommand != nil ) [currentCommand cancel];
[queue makeObjectsPerformSelector:@selector(cancel)];
[queue removeAllObjects];
[lock unlockWithCondition:COMMAND_AVAILABLE];
}
- (void)terminate
{
[threadTerminated lock];
[threadTerminated unlockWithCondition:THREAD_SHOULD_STOP];
[lock lock];
if( currentCommand != nil ) [currentCommand cancel];
[queue makeObjectsPerformSelector:@selector(cancel)];
[queue removeAllObjects];
[lock unlockWithCondition:COMMAND_AVAILABLE];
[threadTerminated lockWhenCondition:THREAD_STOPPED];
[threadTerminated unlock];
}
- (BOOL)itemsToBeProcessed
{
BOOL result = NO;
if( currentCommand != nil ) return YES;
[lock lock];
if( [queue count] > 0 ) result = YES;
[lock unlockWithCondition:COMMAND_AVAILABLE];
return result;
}
@end
@implementation CommandQueue(Private)
- (void)run:(id)object
{
NSAutoreleasePool *thePool = [[NSAutoreleasePool alloc] init];
NSAutoreleasePool *theCommandPool;
do {
theCommandPool = [[NSAutoreleasePool alloc] init];
if( ( currentCommand = [self next] ) != nil ) {
NS_DURING {
[observers makeObjectsPerformSelector:@selector(update:)
withObject:self];
}
NS_HANDLER {
} NS_ENDHANDLER;
NS_DURING {
[currentCommand execute];
}
NS_HANDLER {
} NS_ENDHANDLER;
[currentCommand cancel];
currentCommand = nil;
NS_DURING {
[observers makeObjectsPerformSelector:@selector(update:)
withObject:self];
}
NS_HANDLER {
} NS_ENDHANDLER;
}
[theCommandPool release];
} while( [threadTerminated condition] != THREAD_SHOULD_STOP );
[threadTerminated lock];
[threadTerminated unlockWithCondition:THREAD_STOPPED];
[thePool release];
}
- (id<CommandProtocol>)next
{
id theObject;
[lock lockWhenCondition:COMMAND_AVAILABLE];
NS_DURING {
if( ( theObject = [queue objectAtIndex:0] ) != nil )
[[theObject retain] autorelease];
[queue removeObjectAtIndex:0];
}
NS_HANDLER {
theObject = nil;
} NS_ENDHANDLER;
[lock unlockWithCondition:( [queue count] == 0 ? NO_COMMAND :
COMMAND_AVAILABLE )];
return theObject;
}
@end
I use the observers to spin a chasing arrows view but an indeterminate
progress bar would do just fine. If you download Only Mortal you will
see it running. The current build is at
http://homepage.mac.com/only_mortal/.
Thanks for your suggestions
Message: 1
From: "Mike Davis" <email@hidden>
To: <email@hidden>
Subject: Re: Cocoa idiom for time consuming tasks
Date: Mon, 4 Feb 2002 00:46:46 -0500
In Only Mortal and in ViaVoice I push long tasks into a thread using
the
command pattern. For stopping the commands in the queue, I remove the
waiting commands and "cancel" the current command. I use cancelable
threads.
To call back to the main thread, from the command, I use a
NSConnection. The
docs for NSConnection have an example, though you'll need to do some
more to
make it useful in a real application. Add in a timeout on the
connection,
both ends, and exception handling.
-----------
Joe Chan
email@hidden
http://www.firstian.com
---
Only Mortal, the internet game server browser for MacOS X.
Visit:
http://homepage.mac.com/only_mortal/