• Open Menu Close Menu
  • Apple
  • Shopping Bag
  • Apple
  • Mac
  • iPad
  • iPhone
  • Watch
  • TV
  • Music
  • Support
  • Search apple.com
  • Shopping Bag

Lists

Open Menu Close Menu
  • Terms and Conditions
  • Lists hosted on this site
  • Email the Postmaster
  • Tips for posting to public mailing lists
Re: Cocoa idiom for time consuming tasks
[Date Prev][Date Next][Thread Prev][Thread Next][Date Index][Thread Index]

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/


  • Prev by Date: IB trashed my Nibs!
  • Next by Date: Re: Speed of Quartz (was: optimizing compilers)
  • Previous by thread: Re: Cocoa idiom for time consuming tasks
  • Next by thread: NSFileHandle and Sockets
  • Index(es):
    • Date
    • Thread