erotsppa
erotsppa

Reputation: 15071

If a method is called at the exact same time twice, how to only execute it once?

We have a method in the iPhone SDK that is a delegate method. Problem is, the OS calls this method twice at the exact same time. This method does some heavy lifting so I don't want to execute the logic twice. What is a good way to detect this and prevent one of the two from running?


Forgot to mention that, it is called from different threads.

Upvotes: 5

Views: 6826

Answers (8)

bbum
bbum

Reputation: 162722

Wow. That answer is correct, but way over-engineered. Just use @synchronized().

Foo.h:

@interface Foo
{
    id expensiveResult;
}
- (void) bar;
@end

Foo.m:

@implementation Foo
- (void) bar
{
    @synchronized(self) {
        if (expensiveResult) return expensiveResult;
        .... do expensive stuff here ....
        expensiveResult = [theResult retain];
    }
    return expensiveResult;
}
@end

If you have multiple instances of Foo and want to guarantee exclusivity across all instances, create a global variable in +(void)initialize -- an NSString will do fine -- and @synchronized() on that.

However, your question raises a much more important question. In particular, there is never a case where the same method is going to be called twice simultaneously unless you quite explicitly configured your application to cause exactly that to happen.

The answer(s) provided sound more like a fix to a symptom and not a fix for the real problem.

Note: This is relying on expensiveResult being nil, which it will be as all iVars are nil on instantiation. Obviously, reset the ivar to nil if you want to recalculate.

Upvotes: 5

Adam Rosenfield
Adam Rosenfield

Reputation: 400642

Use pthread_once() -- it was explicitly designed to do exactly this. The only problem is that the function you pass it can't take any arguments, so you have to use global variables to pass information to it.

Upvotes: 0

nall
nall

Reputation: 16149

One method is a BOOL member that you set when entering the method and clear on leaving it. If the variable is set upon entry, you know it's already executing and can just return.

Assuming you're being called from multiple threads, you'll want to lock access to this critical area of checking/setting. An NSLock is good for this.

The code below has two implementations: myMethod1 which uses NSLock and myMethod2 which shows using @synchronize.

@interface MyClass : NSObject
{
    NSLock* theLock;
    BOOL isRunning;
}
@end

@implementation MyClass

-(id)init
{
    self = [super init];
    if(self != nil)
    {
        theLock = [[NSLock alloc] init];
        isRunning = NO;
    }
    return self;
}

-(void)dealloc
{
    [theLock release];
    [super dealloc];
}

// Use NSLock to guard the critical areas
-(void)myMethod1
{
    [theLock lock];

    if(isRunning == YES)
    {
        [theLock unlock]; // Unlock before returning
        return;
    }

    isRunning = YES;        

    // Do fun stuff here

    isRunning = NO;

    [theLock unlock];    
}

// This method uses @synchronize
-(void)myMethod2
{
    @synchronized(self)
    {
        if(isRunning == YES)
        {
            return;
        }

        isRunning = YES;

        // Do stuff here.

        isRunning = NO;
    }
}
@end

Upvotes: 10

Sam Hendley
Sam Hendley

Reputation: 871

Some of the given answers are acceptable solutions to the problem of multiple "producer" threads calling the same function at the same time but you might be better off figuring out why multiple threads are calling this same block of code at the same time. It could be that you are assigning this delegate to multiple event handlers or something like that. You have probably noticed that this is occurring because some shared state is being mangled or the output of the function is not correct for the "global" state at the end of the function. Putting a bandaid over the fact 2 threads are in a given function (when its clear that threading was not a primary concern when this was written) is not going to necessarily give you the right results. Threading is not trivial and shared state makes it very tricky to get right, make sure that you completely understand why this is occurring before just trying to patch over it.

That being said, if you do take the bandaid approach, its probably better to do one with a lock where every thread eventually gets to execute the function rather than having them bail out if the function is allready started because to the client code it would look like that long and "heavy-lifting" process has completed and they may check for the results.

Upvotes: 2

Malaxeur
Malaxeur

Reputation: 6043

Here's how you can use objective-c locks as mutexes.

http://rosettacode.org/wiki/Mutex#Objective-C

Mutexes exist to allow mutually exclusive access to a certain block of code. Within the method you can do something like:

[methodLock lock]; //Blocks and waits until the lock is released
//...do stuff
[methodLock unlock]; //Releases the lock once the code has executed.

This will ensure that only one thread will be allowed within the //do stuff block of code.

EDIT: I read the question again; within the lock I'd check the flag to see if it's run (using a BOOL)

Upvotes: 0

coneybeare
coneybeare

Reputation: 33101

simplest is to set a flag.

- (void)action {
  if (flag_is_set == NO) {
    flag_is_set = YES;
    do stuff
    flag_is_set = NO;
  }
}

this is not 100% safe though as you may get some rare cases of interlocking.

If you can handle some sleeps on the thread, use a nslock

- (id)init {
  theLock = [[NSLock alloc] init];
}
- (void)action {
  [theLock lock];
  doStuff
  [theLock unlock];
}

When thread 2 comes to the lock call and thread 1 already has it, it will sit in the execution loop until the lock is released, then it will start again. If you have UI on this thread, you app will appear to freeze

Upvotes: 2

If they are called at the exact same time, I guess they are called in threads?

What you can do is define a BOOL @property (called isRunning for example) with the attribute atomic (set by default). This way this property can be accessed safely from different threads, and then you can do something like:

if (isRunning)
    return ;
isRunning = YES;
// ...
// Your code here
// ...
usRunning = NO;

You might also make sure that you are doing the right thing. If your method is called twice, maybe you're doing something wrong (or maybe it's normal, I don't know what you are doing ;))

Upvotes: -1

Warren Young
Warren Young

Reputation: 42373

If these calls are synchronized, so only one happens at a time, you can just have a variable on your class, called something like "initialized", which starts off false and is set when initialized:

if (!initialized) {
    // handle delegated call
    initialized = 1;
}

If the delegate is called from multiple threads, you need to put all this in a mutex block.

Upvotes: 0

Related Questions