3

I have this (rare) odd case where my objective-c iOS program is locking up. When I break into the debugger, there are two threads and both of them are stuck at a @synchronized().

Unless I am completely misunderstanding @synchronized, I didn't think that was possible and the whole point of the command.

I have a main thread and worker thread that both need access to a sqlite database, so I wrap the chunks of code that are accessing the db in @synchronized(myDatabase) blocks. Not much else happens in these blocks except the db access.

I'm also using the FMDatabase framework to access sqlite, I don't know if that matters.

The myDatabase is a global variable that contains the FMDatabase object. It is created once at the start of the program.

Roger Gilbrat
  • 3,755
  • 5
  • 34
  • 58
  • Just to narrow down your problem - try replacing your @synchronized to [lock lock] and [lock unlock] on a shared NSLock instance. – Stavash Apr 15 '12 at 15:53
  • That's easier said than done. :-) This lockup happens about once a week with hundreds of hours on use in between. It's also my understanding that what you mention is exactly what @synchronized does internally. – Roger Gilbrat Apr 15 '12 at 16:10
  • Well, not exactly. For one it's significantly faster. Take a look at http://perpendiculo.us/?p=133 – Stavash Apr 15 '12 at 16:13
  • 3
    Do you ever synchronize against another object, or use a shared lock? If so, check to make sure you don't have nested locks with one thread synchronizing on object A then object B and the other thread doing the opposite--this will cause the behavior you describe. – andyvn22 Apr 15 '12 at 17:01
  • 1
    Another tip - hit the pause button on the XCode debugger to figure out which thread is where once the suspected deadlock occurs. – Stavash Apr 15 '12 at 17:05
  • Are they `@synchronized` using the same object or different ones? It's possible to deadlock using two different objects. – joerick Apr 15 '12 at 21:34
  • They are all on the same object. – Roger Gilbrat Apr 16 '12 at 01:13

2 Answers2

5

I know I'm late to the party with this, but I've found a strange combination of circumstances that @synchronized handles poorly and is probably responsible for your problem. I don't have a solution to it, besides to change the code to eliminate the cause once you know what it is.

I will be using this code below to demonstrate.

- (int)getNumberEight {
    @synchronized(_lockObject) {
        // Point A
        return 8;
    }
}

- (void)printEight {
    @synchronized(_lockObject) {
        // Point B
        NSLog(@"%d", [self getNumberEight]);
    }
}

- (void)printSomethingElse {
    @synchronized(_lockObject) {
        // Point C
        NSLog(@"Something Else.");
    }
}

Generally, @synchronized is a recursively-safe lock. As such, calling [self printEight] is ok and will not cause deadlocks. What I've found is an exception to that rule. The following series of events will cause deadlock and is extremely difficult to track down.

  1. Thread 1 enters -printEight and acquires the lock.
  2. Thread 2 enters -printSomethingElse and attempts to acquire the lock. The lock is held by Thread 1, so it is enqueued to wait until the lock is available and blocks.
  3. Thread 1 enter -getNumberEight and attempts to acquire the lock. The lock is held already and someone else is in the queue to get it next, so Thread 1 blocks. Deadlock.

It appears that this functionality is an unintended consequence of the desire to bound starvation when using @synchronized. The lock is only recursively safe when no other thread is waiting for it.

The next time you hit deadlock in your code, examine the call stacks on each thread to see if either of the deadlocked threads already holds the lock. In the sample code above, by adding long sleeps at Point A, B, and C, the deadlock can be recreated with almost 100% consistency.

EDIT:

I'm no longer able to demonstrate the previous issue, but there is a related situation that still causes issues. It has to do with the dynamic behavior of dispatch_sync.

In this code, there are two attempts to acquire the lock recursively. The first calls from the main queue into a background queue. The second calls from the background queue into the main queue.

What causes the difference in behavior is the distinction between dispatch queues and threads. The first example calls onto a different queue, but never changes threads, so the recursive mutex is acquired. The second changes threads when it changes queues, so the recursive mutex cannot be acquired.

To emphasize, this functionality is by design, but it behavior may be unexpected to some that do not understand GCD as well as they could.

dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
NSObject *lock = [[NSObject alloc] init];
NSTimeInterval delay = 5;

NSLog(@"Example 1:");
dispatch_async(queue, ^{
    NSLog(@"Starting %d seconds of runloop for example 1.", (int)delay);
    [[NSRunLoop currentRunLoop] runUntilDate:[NSDate dateWithTimeIntervalSinceNow:delay]];
    NSLog(@"Finished executing runloop for example 1.");
});
NSLog(@"Acquiring initial Lock.");
@synchronized(lock) {
    NSLog(@"Acquiring recursive Lock.");
    dispatch_sync(queue, ^{
        NSLog(@"Deadlock?");
        @synchronized(lock) {
            NSLog(@"No Deadlock!");
        }
    });
}

NSLog(@"\n\nSleeping to clean up.\n\n");
sleep(delay);

NSLog(@"Example 2:");
dispatch_async(queue, ^{
    NSLog(@"Acquiring initial Lock.");
    @synchronized(lock) {
        NSLog(@"Acquiring recursive Lock.");
        dispatch_sync(dispatch_get_main_queue(), ^{
            NSLog(@"Deadlock?");
            @synchronized(lock) {
                NSLog(@"Deadlock!");
            }
        });
    }
});

NSLog(@"Starting %d seconds of runloop for example 2.", (int)delay);
[[NSRunLoop currentRunLoop] runUntilDate:[NSDate dateWithTimeIntervalSinceNow:delay]];
NSLog(@"Finished executing runloop for example 2.");
Holly
  • 5,270
  • 1
  • 24
  • 27
  • Using your example code (and +[NSThread sleepForTimeInterval:] ranging from 0.1 to 20.0) at Point A, B and C I am not able to reproduce the problem at all (using iOS SDK7.1 as well as OS X SDK 10.9). Can you be more specific on how to recreate the problem? – Alexander Battisti Jul 29 '14 at 09:12
  • I'm not able to reproduce this either (using iOS 9 with Xcode 7). Doesn't sound right, anyway — `@synchronized` was not designed to be "generally" safe with multiple locks on the same object; it was designed to be *totally* safe with multiple locks (on the same thread). If you really do have a case where this is a demonstratable issue, it would be very good to submit it to Apple (full working program to demonstrate the error) as a bug report. – Todd Lehman Mar 30 '16 at 00:27
  • I'm really surprised that this is still getting attention. I originally noticed this issue on iOS 4.x and 5.x many years ago. This was written back in 2013. It may be resolved with improvements to the compiler and Objective C runtime since then. – Holly Mar 30 '16 at 02:12
0

I stumbled into this recently, assuming that @synchronized(_dataLock) does what it's supposed to do, since it is such a fundamental thing after all.

I went on investigating the _dataLock object, in my design I have several Database objects that will do their locking independently so I was simply creating _dataLock = [[NSNumber numberWithInt:1] retain] for each instance of Database.
However the [NSNumber numberWithInt:1] returns the same object, as in same pointer!!!

Which means what I thought was a localized lock for only one instance of Database is not a global lock for all instances of Database.
Of course this was never the intended design and I am sure this was the cause of issues.

I will change the

_dataLock = [[NSNumber numberWithInt:1] retain] 

with

_dataLock = [[NSUUID UUID] UUIDString] retain]
Klajd Deda
  • 355
  • 2
  • 7
  • "I will change" makes me think you are not sure this will solve the problem. In any case, it looks like you haven't really tried it. Anyway I think a sentence is contradictory: "what I thought was a localized lock for only one instance of Database is not a global lock for all instances of Database". Are you sure you wanted to say "what I thought was local is not global"? It doesn't make much sense to me. Maybe you meant "What I thought was local is global"? If it is so, please edit your answer and fix it. Thank you! – Fabio says Reinstate Monica Nov 23 '15 at 18:10