Skip to main content
GameDev.net gamedev.net
🔒 Locked 🎮 Unity

Multi thread deadlock issue with a recursive mutex. Need ideas.

Started by Chindril Mar 20, 2016 at 12:39 AM 20 replies 15.3k views
Original Post
Chindril
Chindril

I have a pretty big issue right now with a dead lock in a multi threaded software. I know which of my threads and mutexes cause the deadlock but I do not understand why.

I have a setup that looks like this (Pseudo c++ code)

AutoLock is a scoped lock class, nothing fancy.


class Foo
{
public:
    void start();   //Spawns the thread that will call run()

    void doStuffA() {AutoLock lock(&mutexA); <DoStuffHere()> }
    void doStuffB() {AutoLock lock(&mutexB); <DoStuffHere()> }

private:
    void extraWork()
    {
        AutoLock lock(&mutexB);
        
        //Processing here

        doStuffB();
    }

    void run()   //Threaded function
    {
        while(true)
        {
            AutoLock lock(&mutexB);   //Lock the B Mutex

            //Do a lot of work, networking stuff, etc
            extraWork();   //This is fine since we're using a recursive mutex

            AutoLock lock2(&mutexA); //Dead lock here after a couple of hours of run time.
        }
    }

    boost::recursive_mutex mutexA;
    boost::recursive_mutex mutexB;
};


int main()
{
    Foo foo;
    foo.start();

    while(true)
    {
        foo.doStuffA();
        //Do stuff
        foo.doStuffA();
        //Do stuff
        foo.doStuffA();

        //Do some stuff

        foo.doStuffB();    //Usually hangs here a bit while foo finishes a loop
    }
}


So the code is obviously not exactly like this but the logic is the same. We ran this code with no problems for a long time and it just recently starting deadlocking. We traced the code with dumps and know this setup is causing the problem.

Note that the main thread cannot lock A or B mutex directly but only by calling public functions of Foo. Since we lock only using the AutoLock class (Scoped lock), the main thread should never keep a lock on the mutexes. Yet, the thread sometimes hangs indefinitely when trying to lock mutex A.

I know from looking at boost code that the hanging only happens if the current thread id inside the mutex is different from the one calling ->lock(). Therefore there's only 2 explanations to this problem.

1. The main thread somehow keeps a lock on mutex A.
2. There's memory corruption that messes the data of the mutex A.

I'm really out of ideas and if some multithreading guru could give me tips on what to look for it would be greatly appreciated.

nfries88
nfries88

I wouldn't call myself a guru, but I've been touching concurrent code for over a decade, a few years ago I wised up and avoid techniques like you're employing here though.

1) I recommend you don't use recursive locks. You'll actually get better concurrent performance if you release the lock around function calls. Remember: locks prevent concurrency. You want to hold a lock as briefly as possible.
2) You have two threads that could possibly be holding this lock, right? So check the thread holding the lock. recursive_mutex is implemented in headers, just find your system's implementation and change recursive_mutex::owner's visibility to public, then do the same owner check your system's recursive_mutex implementation does. If neither thread matches, you must have memory corruption. If the main thread matches, you're not releasing the lock somewhere. If the "Foo" thread matches, I don't know what the problem is.

Hodgman
Hodgman

DoStuffHere doesn't touch mutexA/mutexB in any way?

Yeah it could be a memory corruption issue... Though I've had incorrect threading code actually work fine and consistently pass unit test for a year, before it decided to fail the unit tests once. An audit of the code did find a subtle logic bug...

You can see if this "fixes" the issue:

int pad1[1024];
    boost::recursive_mutex mutexA;
int pad2[1024];
    boost::recursive_mutex mutexB;
int pad3[1024];

Why is mutexA being locked/unlocked at the end of that loop anyway?

Chindril
Chindril

@nfries88: I admit I should look back at the whole class to see if recursive mutex are really needed, but it is useful when your code is split in multiple functions and each one of those needs the lock.

As for your 2nd point, the main thread does lock the mutexes throught the Foo class but never access them directly. Technically it cannot keep a lock on a mutex when the stack isn't in the Foo class. We actually updated our code to save the recusive mutex information about thread info so we will see if it is a memory corruption.

@Hodgman: DoStuffHere isn't a function, just to say there's some code in there that the mutex protect. I agree the code seems ridiculous because I removed most of the important processing. Would it make more sense if I said the Lock on MutexA at the end of a thread loop is really coming from a network event processed that needs to modify variables protected by that mutex ?

I'll try the patch of padding the mutexes with char buffers. I didn't think about it but it might point me to the correct direction.

Thank you both.

nfries88
nfries88

@nfries88: I admit I should look back at the whole class to see if recursive mutex are really needed, but it is useful when your code is split in multiple functions and each one of those needs the lock.

It makes cleaner code at the cost of performance.
The only reasonable argument I could hear in favor of recursive mutex stems from a genuine need for a guarantee of non-interruption, but in this case a second function can be used to achieve the same effect, IE:
(obviously a sane implementation would do data processing in the function. This is just an example)


class Example
{
public:
    void SimpleTask(){ lock(); UnsafeSimpleTask(); unlock();
    void ComplexTask(){ lock(); data->process(); UnsafeSimpleTask(); data->processMore(); unlock(); }
private:
    void UnsafeSimpleTask(){ data->processYetAgain(); }
    Data *data;
}
ankhd
ankhd

Hi.

It could be because the function extraWork you call doworkB which locks b but b is locked already in the call to extraWork.

Maybe boost::strands could help you here. or lockfree queue's. The only place for mutex is in the connection phase of the networking, where you have simultaneous clients connecting to a data base or some thing even then you could go lock free as well.

frob
frob

            AutoLock lock(&mutexB);   //Lock the B Mutex

            //Do a lot of work, networking stuff, etc
extraWork(); //This is fine since we're using a recursive mutex

            AutoLock lock2(&mutexA); //Dead lock here after a couple of hours of run time.

Right there you acquire multiple locks. That is a common source of deadlocks. Additionally you are holding a lock for a long time to "do a lot of work" and "networking stuff" which is going to be slow. Holding locks over time is a good way to starve your system.

A common problem in multiple locks is getting ordering wrong. One locks A then B, another place you lock B then A. It may take some time to stumble over it, but you've just introduced a deadlock by doing them in a different order. One system holds A, the other holds B, and neither will release as they're waiting for each other.

When you've got multiple locks in place you need a way to attempt to get all the locks all at once, and if you cannot get all of them, back off and wait until all are available, then repeat.

Lock ordering and lock hierarchies help reduce the difficulties in this type of tasks.

Several systems I've worked with used a class do that work. Every lock is named and numbered in code as an enumeration. You can request a set of locks used by the block as a single call, or acquire a single lock if you don't have any other locks out. Internally you can only add a higher-number lock, never add a lower-number lock, which helps preserve ordering; attempting to add a lower-priority lock triggers all kinds of alerts and logging. If acquiring multiple locks the system can attempt to gain them all, and on failure release back down the tree and wait for availability. Finally, if a system holds a lock for longer than a specified time, alerts and logging are triggered.

nfries88
nfries88

I'm not sure how I missed it before, but Frob's comment seems to have shaken the gunk from my brain.

Based on what you've shared, the most probable location of a deadlock is in Foo::doStuffA or functions it calls. Either:
1) You have an infinite loop.
2) You're locking mutexB.
3) You're stuck on another deadlock that's causing this deadlock.

Chindril
Chindril

@ankhd: That's why a recursive mutex is used. As long as you are in the same thread you can look a mutex any number of times. It is reference counted and the lock is released only when all of the unlock() functions are called.

@frob: I was burned in the past with the ordering of multiple mutex locks and that's the first thing I look for when I have a multi thread. I suppose this code is risky and should be done in a better way. I never thought before of a class to handle multiple locks to prevent ordering issues but it sounds like a good idea for the future.

I need to review a larger scope of the code since the cause of this deadlock isn't obvious at first sight. I got 3 programmer to check the code and all of them think a deadlock shouldn't be happening. Perhaps the problem is elsewhere and the deadlock is a symptom of a bigger problem. Anyhow I'll keep in mind all of your suggestions and keep you posted when we find the issue :).

Thanks.

frob
frob




all of them think a deadlock shouldn't be happening.

A single lock is not a problem.

Any time you acquire more than one lock a deadlock is not only possible, it is likely going to happen unless a try-else-rollback system is in place.

Chindril
Chindril

A thread that acquires multiple locks is not a problem as long as all other threads acquires at most a single one of these locks at the same time. Am I correct in this assumption ?

Pink Horror
Pink Horror




I need to review a larger scope of the code since the cause of this deadlock isn't obvious at first sight. I got 3 programmer to check the code and all of them think a deadlock shouldn't be happening. Perhaps the problem is elsewhere and the deadlock is a symptom of a bigger problem. Anyhow I'll keep in mind all of your suggestions and keep you posted when we find the issue .


In practically every one of these problems on gamedev I've tried to figure out - where someone posts a small section of code and expects the community to offer solutions - the problem is not in the small section of code posted.

When you're in the deadlock, you should be able to halt the program in the debugger, look at all the locks and the call stacks of all of your threads, and figure out the problem. These problems can sometimes be interesting to try to guess, but you're the one in position to fix it.

At the beginning, you mentioned how the boost code has a thread id inside. So, first, reproduce your deadlock. What is that id during the lock? What are the ids of all your threads? Have you ruled out the memory problem yet?
Chindril
Chindril

@Pink Horror You are absolutely right, I was more trying to get ideas and brainstorm on this subject. Perhaps I overlooked something that someone could point me out. I understand it is very complicated but I can't release thousands of line of codes and hope someone does my job haha.

As for the debugging, the problem is that the freeze does not reproduce on our local test site, only at the customer's. We work on Windows with Visual Studio 2013 but I cannot install it on the target machine (Windows XP...) to attach the debugger.

So we are using ProcDump to generate a full memory dump file (using -ma), however when debugging it is missing some memory information for reasons I do not understand. I'm out of luck since I don't have the memory information of the mutex to retrieve the thread ID.

frob
frob

A thread that acquires multiple locks is not a problem as long as all other threads acquires at most a single one of these locks at the same time. Am I correct in this assumption ?


Not necessarily. If one thread has A, another thread has B, and you are waiting for both A and B, it is possible you may not get both for a very long time, if ever.

Repeating what I said in words above with pseudocode below, generally with a set of locks you use something like this:
BlockUntilLocked( collectionOfLocks ) {
  while (!TryToGetAllLocks( collectionOfLocks ) {
    /* Note that you may be waiting a long, long time. */
    WaitForObjects(collectionOfLocks);
  };
}

bool TryToGetAllLocks( collectionOfLocks ) {
  if( !MyThread().LockController.CanGetTheseLocks( collectionOfLocks ) ) {
    Error( "Attempting to get get a lock when we shouldn't. This will probably deadlock eventually. Fix it.");
    /* Probably exit the function, or otherwise throw a noisy alert. */
  }
  bool allLocked = true;
  collectionOfLocks.sortByLockPriority();
  foreach( lock in collectionOfLocks ) {
    if(!TryGetLock( lock ) ) {
      allLocked = false;
    }
  }
  if(!allLocked) {
    foreach( lock in collectionOfLocks ) {
      lock.Unlock();
    }
  }
  return allLocked;
}
nfries88
nfries88

@Pink Horror You are absolutely right, I was more trying to get ideas and brainstorm on this subject. Perhaps I overlooked something that someone could point me out. I understand it is very complicated but I can't release thousands of line of codes and hope someone does my job haha.

As for the debugging, the problem is that the freeze does not reproduce on our local test site, only at the customer's. We work on Windows with Visual Studio 2013 but I cannot install it on the target machine (Windows XP...) to attach the debugger.

So we are using ProcDump to generate a full memory dump file (using -ma), however when debugging it is missing some memory information for reasons I do not understand. I'm out of luck since I don't have the memory information of the mutex to retrieve the thread ID.

Sounds like you're down to the last and least savory option: add logging for your locks.
It's a good idea to add this AFTER each construction of your AutoLock class, so you can get line and filename information. IE:

// in some header:
extern void LogLocking(const char * lockname, const char * filename, int lineno);
#define LOG_LOCK(lock) LogLocking(#lock , __FILE__ , __LINE__-1);

...
// in some source file:
#include <cstdio>
boost::mutex loglock;
FILE *log = NULL;
void LogLocking(const char * lockname, const char * filename, int lineno)
{
    loglock.lock();
    if(!log)
    {
         char fname[256];
         sprintf(fname, "lock log - %x.txt", std::time(nullptr));
         log = fopen(fname, "wt+");
    }
    assert(log != nullptr);
    fprintf(log, "locking %s at line %d in %s", lockname, lineno, filename);
    fflush(log);
    loglock.unlock();
}
the locking is necessary, but that should never deadlock.
remove assert if you want.
this will tell you the last place your lock was locked successfully, so somewhere after that is going to be where the lock is not released and the source of your deadlock
unfortunately it doesn't tell you other useful information like if and where it was unlocked - I figure this can get really wonky with the use of recursive mutexes, but there's no simple way around it with your use of a RAII scoped locking class.
I'm using fprintf to minimize the performance hit of logging - fprintf is significantly faster than doing the same thing with any implementation of ofstream I've come across.
I'm forcing a buffer flush just because it's possible that the very next attempted lock acquisition is the deadlock, in which case the buffer flush won't ever happen, and you need that info
Chindril
Chindril

An update on this topic, our bugs were found and fixed. However this thread was me looking in the wrong direction. The deadlock happening was a side-effect of the real bug and not the source of my issues. The bug was coming from a system was acting erratically and sending way too many messages over the network and our code could not keep up and they would stack up. Overtime, having a ton of very small memory blocks for each message would fragment the memory and then various systems would fail. The deadlock was the most common effect but we also had thread initialization failing and sometimes a straight up memory allocation failure (malloc of a big chunk returns NULL and that pointer was then used).

Anyhow, a lot of time was wasted looking in the wrong direction but we learned a lot about our code base so it was not all in vain.

Thanks for your help !

(by the way I'm not sure if there's a way to tag this thread was closed, or solved, or something alike)

Lactose
Lactose

(by the way I'm not sure if there's a way to tag this thread was closed, or solved, or something alike)

We don't tag threads as closed/solved.

Hello to all my stalkers.
Pink Horror
Pink Horror

An update on this topic, our bugs were found and fixed. However this thread was me looking in the wrong direction. The deadlock happening was a side-effect of the real bug and not the source of my issues. The bug was coming from a system was acting erratically and sending way too many messages over the network and our code could not keep up and they would stack up. Overtime, having a ton of very small memory blocks for each message would fragment the memory and then various systems would fail. The deadlock was the most common effect but we also had thread initialization failing and sometimes a straight up memory allocation failure (malloc of a big chunk returns NULL and that pointer was then used).

So, you have memory allocation failures, and most of them do not crash your program immediately? And then you're stuck dealing with other bugs that look impossible? Let me guess, you have catch (exception) or, even worse, catch (...) everywhere, with maybe a log saying "unknown exception" if your programmers are slightly less lazy than the people who just leave the catch empty?

nfries88
nfries88

An update on this topic, our bugs were found and fixed. However this thread was me looking in the wrong direction. The deadlock happening was a side-effect of the real bug and not the source of my issues. The bug was coming from a system was acting erratically and sending way too many messages over the network and our code could not keep up and they would stack up. Overtime, having a ton of very small memory blocks for each message would fragment the memory and then various systems would fail. The deadlock was the most common effect but we also had thread initialization failing and sometimes a straight up memory allocation failure (malloc of a big chunk returns NULL and that pointer was then used).

So, you have memory allocation failures, and most of them do not crash your program immediately? And then you're stuck dealing with other bugs that look impossible? Let me guess, you have catch (exception) or, even worse, catch (...) everywhere, with maybe a log saying "unknown exception" if your programmers are slightly less lazy than the people who just leave the catch empty?

malloc does not throw exceptions. It simply returns NULL. On some systems, there is no built-in catch for NULL dereferences, and NULL+offsetof(SomeStruct, someMember) might reasonably point to memory used by the main thread's stack, some global variable, allocation records, or even the OS itself; any of which could have very unpredictable consequences. It's entirely possible no C++ exception was ever thrown and no OS exception/signal/etc was ever triggered, and memory was silently corrupted.


@OP: I assume you corrected these side-effects by checking the address returned by malloc, and probably corrected the system that was acting up. But you should still do something about the heap fragmentation. If one system sending too many messages causes a heap fragmentation, what happens when several systems start having to send that many messages just to handle their workloads? It might be something you can put off for awhile, but you may find yourself needing to resolve the fragmentation issue in the future, I'd treat this like an early warning.

Pink Horror
Pink Horror

An update on this topic, our bugs were found and fixed. However this thread was me looking in the wrong direction. The deadlock happening was a side-effect of the real bug and not the source of my issues. The bug was coming from a system was acting erratically and sending way too many messages over the network and our code could not keep up and they would stack up. Overtime, having a ton of very small memory blocks for each message would fragment the memory and then various systems would fail. The deadlock was the most common effect but we also had thread initialization failing and sometimes a straight up memory allocation failure (malloc of a big chunk returns NULL and that pointer was then used).

So, you have memory allocation failures, and most of them do not crash your program immediately? And then you're stuck dealing with other bugs that look impossible? Let me guess, you have catch (exception) or, even worse, catch (...) everywhere, with maybe a log saying "unknown exception" if your programmers are slightly less lazy than the people who just leave the catch empty?

malloc does not throw exceptions. It simply returns NULL. On some systems, there is no built-in catch for NULL dereferences, and NULL+offsetof(SomeStruct, someMember) might reasonably point to memory used by the main thread's stack, some global variable, allocation records, or even the OS itself; any of which could have very unpredictable consequences. It's entirely possible no C++ exception was ever thrown and no OS exception/signal/etc was ever triggered, and memory was silently corrupted.

Sure, that's possible, but I still think that it's relatively unlikely to have memory corruption de-referencing null pointers from failed malloc calls, instead of segmentation faults, compared to the chance this code is throwing and catching bad_alloc exceptions. The code above is clearly C++. I would guess new is being used, even with malloc mentioned earlier.

I've never worked on a program that corrupted memory through an offset null pointer. I have worked on code where memory usage would spike up and cause allocation failures, because it was filled with try/catch statements.

Topic Locked

This topic has been locked by a moderator. New replies are not allowed.

Sign in to reply to this topic.