Reputation: 9875
I need an advice from you guys on the design of my app here, basically I would like to know if it will work as I expect ? As the multi-threading is quite tricky thing I would like to hear from you.
Basically my task is very simple -I've SomeBigSingletonClass
- big singleton class, which has two methods someMethodOne
and someMethodTwo
These methods should be invoked periodically (timer based) and in separate threads.
But there should be only one instance of each thread at the moment, e.g. there should be only one running someMethodOne
at any time and the same for someMethodTwo
.
What I've tried
GCD - Did implementation with GCD but it lacks very important feature, it does not provide means to check if there is any running task at the moment, i.e. I was not able to check if there is only one running instance of let say someMethodOne
method.
NSThread - It does provide good functionality but I'm pretty sure that new high level technologies like NSOperation and GCD will make it more simple to maintain my code. So I decided to give-up with NSThread.
My Solution with NSOperation
How I plan to implement the two thread invokation
@implementation SomeBigSingletonClass
- (id)init
{
...
// queue is an iVar
queue = [[NSOperationQueue alloc] init];
// As I'll have maximum two running threads
[queue setMaxConcurrentOperationCount:2];
...
}
+ (SomeBigSingletonClass *)sharedInstance
{
static SomeBigSingletonClass *sharedInstance = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
sharedInstance = [[SomeBigSingletonClass alloc] init];
});
return sharedInstance;
}
- (void)someMethodOne
{
SomeMethodOneOperation *one = [[SomeMethodOneOperation alloc] init];
[queue addOperation:one];
}
- (void)someMethodTwo
{
SomeMethodTwoOperation *two = [[SomeMethodOneOperation alloc] init];
[queue addOperation:two];
}
@end
And finally my NSOperation inherited class will look like this
@implementation SomeMethodOneOperation
- (id)init
{
if (![super init]) return nil;
return self;
}
- (void)main {
// Check if the operation is not running
if (![self isExecuting]) {
[[SomeBigSingletonClass sharedInstance] doMethodOneStuff];
}
}
@end
And the same for SomeMethodTwoOperation
operation class.
Upvotes: 3
Views: 1971
Reputation: 28409
Sorry, I'm late to the party. If your methods are called back based on timers, and you want them to execute concurrently with respect to one another, but synchronous with respect to themselves, might I suggest using GCD timers.
Basically, you have two timers, one which executes methodOne, and the other executes methodTwo. Since you pass blocks to the GCD timers, you don't even have to use methods, especially if you want to make sure other code does not call those methods when they are not supposed to run.
If you schedule the timers onto a concurrent queue, then both timers could possibly be running at the same time on different threads. However, the timer itself will only run when it is scheduled. Here is an example I just hacked up... you can easily use it with a singleton...
First, a helper function to create a timer that takes a block which will be called when the timer fires. The block passes the object, so it can be referenced by the block without creating a retain cycle. If we use self as the parameter name, the code in the block can look just like other code...
static dispatch_source_t setupTimer(Foo *fooIn, NSTimeInterval timeout, void (^block)(Foo * self)) {
// Create a timer that uses the default concurrent queue.
// Thus, we can create multiple timers that can run concurrently.
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
dispatch_source_t timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, queue);
uint64_t timeoutNanoSeconds = timeout * NSEC_PER_SEC;
dispatch_source_set_timer(timer,
dispatch_time(DISPATCH_TIME_NOW, timeoutNanoSeconds),
timeoutNanoSeconds,
0);
// Prevent reference cycle
__weak Foo *weakFoo = fooIn;
dispatch_source_set_event_handler(timer, ^{
// It is possible that the timer is running in another thread while Foo is being
// destroyed, so make sure it is still there.
Foo *strongFoo = weakFoo;
if (strongFoo) block(strongFoo);
});
return timer;
}
Now, the basic class implementation. If you don't want to expose methodOne and methodTwo, there is no reason to even create them, especially if they are simple, as you can just put that code directly in the block.
@implementation Foo {
dispatch_source_t timer1_;
dispatch_source_t timer2_;
}
- (void)methodOne {
NSLog(@"methodOne");
}
- (void)methodTwo {
NSLog(@"methodTwo");
}
- (id)initWithTimeout1:(NSTimeInterval)timeout1 timeout2:(NSTimeInterval)timeout2 {
if (self = [super init]) {
timer1_ = setupTimer(self, timeout1, ^(Foo *self) {
// Do "methodOne" work in this block... or call it.
[self methodOne];
});
timer2_ = setupTimer(self, timeout2, ^(Foo *self) {
// Do "methodOne" work in this block... or call it.
[self methodTwo];
});
dispatch_resume(timer1_);
dispatch_resume(timer2_);
}
return self;
}
- (void)dealloc {
dispatch_source_cancel(timer2_);
dispatch_release(timer2_);
dispatch_source_cancel(timer1_);
dispatch_release(timer1_);
}
@end
EDIT In response to the comments (with more detail to hopefully explain why the block will not be executed concurrently, and why missed timers are coalesced into one).
You do not need to check for it being run multiple times. Straight from the documentation...
Dispatch sources are not reentrant. Any events received while the dispatch source is suspended or while the event handler block is currently executing are coalesced and delivered after the dispatch source is resumed or the event handler block has returned.
That means when a GCD dispatch_source timer block is dispatched, it will not be dispatched again until the one that is already running completes. You do nothing, and the library itself will make sure the block is not executed multiple times concurrently.
If that block takes longer than the timer interval, then the "next" timer call will wait until the one that is running completes. Also, all the events that would have been delivered are coalesced into one single event.
You can call
unsigned numEventsFired = dispatch_source_get_data(timer);
from within your handler to get the number of events that have fired since the last time the handler was executed (e.g., if your handler ran through 4 timer firings, this would be 4 - but you would still get all this firings in this one event -- you would not receive separate events for them).
For example, let's say your interval timer is 1 second, and your timer happens to take 5 seconds to run. That timer will not fire again until the current block is done. Furthermore, all those timers will be coalesced into one, so you will get one call into your block, not 5.
Now, having said all that, I should caution you about what I think may be a bug. Now, I rarely lay bugs at the feet of library code, but this one is repeatable, and seems to go against the documentation. So, if it's not a bug, it's an undocumented feature. However, it is easy to get around.
When using timers, I have noticed that coalesced timers will most certainly be coalesced. That means, if your timer handler is running, and 5 timers fired while it was running, the block will be called immediately, representing those missed 5 events. However, as soon as that one is done, the block will be executed again, just once, no matter how many timer events were missed before.
It's easy to identify these, though, because dispatch_source_get_data(timer) will return 0, which means that no timer events have fired since the last time the block was called.
Thus, I have grown accustomed to adding this code as the first line of my timer handlers...
if (dispatch_source_get_data(timer) == 0) return;
Upvotes: 0
Reputation: 16022
If you are using NSOperation
, you can achieve what you want be creating your own NSOperationQueue
and setting numberOfConcurrentOperations
to 1.
You could have also maybe used an @synchronized
scope with your class as your lock object.
EDIT: clarification---
What I am proposing:
Queue A (1 concurrent operation--used to perform SomeMethodOneOperation
SomeMethodTwoOperation
once at a time)
Queue B (n concurrent operations--used for general background operation performing)
EDIT 2: Updated code illustrating approach to run maximum operation one and operation two, with max one each of operation one and operation two executing at any given time.
-(void)enqueueMethodOne
{
static NSOperationQueue * methodOneQueue = nil ;
static dispatch_once_t onceToken ;
dispatch_once(&onceToken, ^{
queue = [ [ NSOperationQueue alloc ] init ] ;
queue = 1 ;
});
[ queue addOperation:[ NSBlockOperation blockOperationWithBlock:^{
... do method one ...
} ] ];
}
-(void)enqueueMethodTwo
{
static NSOperationQueue * queue = nil ;
static dispatch_once_t onceToken ;
dispatch_once(&onceToken, ^{
queue = [ [ NSOperationQueue alloc ] init ] ;
queue = 1 ;
});
[ queue addOperation:[ NSBlockOperation blockOperationWithBlock:^{
... do method two ...
} ] ];
}
EDIT 3:
per our discussion:
I pointed out that isExecuting is a member variable and refers only to the state of the operation being queried, not if any instance of that class is executing
therefore Deimus' solution won't work to keep multiple instances of operation one running simultaneously for example
Upvotes: 2