Locking a mutex multiple times?

Just starting out? Need help? Post your questions and find answers here.
mikejs
Enthusiast
Enthusiast
Posts: 160
Joined: Thu Oct 21, 2010 9:46 pm

Locking a mutex multiple times?

Post by mikejs »

This may be a daft question...

If I have a multi-threaded app, I will need to have a mutex of some sort to ensure that certain data can only be accessed/modified by one thread at a time.

Mostly, this is reasonably straightforward to manage, but there are some cases where it gets a bit tangled. Suppose I have a function that needs the mutex - I can add lock and unlock calls to that, and things will then work as expected:

Code: Select all

Procedure SomeFunction()
  ; Needs Mutex
  LockMutex(m)
  
  ; ... does stuff.
  
  UnlockMutex(m)
EndProcedure
If I need to call that function from somewhere in my code that does not itself need the mutex, and so does not do its own lock/unlock, then this will work well:

Code: Select all

  ; Ordinary code, no mutex needed.
  
  ; SomeFunction() will handle the mutex for itself
  SomeFunction()
  
  ; Ordinary code, no mutex needed.
But, what happens if I also need to call that function from elsewhere in the code that does need the mutex for its own activities, and so has already called LockMutex() before it gets to calling SomeFunction()?

Code: Select all

  ; Needs Mutex
  LockMutex(m)
  
  ; ... does stuff that needs the mutex
  
  ; Needs SomeFunction
  SomeFunction()
  
  ; ... continues doing stuff that needs the mutex
  
  ; Done now
  UnlockMutex(m)
What would happen here is two calls to LockMutex() followed by two calls to UnlockMutex(). The question is whether that will result in the desired behaviour (only really unlocking on the second UnlockMutex() call). The documentation doesn't say either way as far as I can see, and although I could try it, if it's undefined behaviour I wouldn't want to rely on the result.

Essentially, if the locking doesn't act like a counter, the options as I see it are:
  • Leave the lock/unlock in SomeFunction(). Callers of SomeFunction must call UnlockMutex() first if they had locked it. This may be a bad idea if they have work in progress that they need to complete before unlocking.
  • Take the lock/unlock out of SomeFunction(). Callers of SomeFunction must call LockMutex() before and UnlockMutex() after if they weren't already doing so. If SomeFunction() is called from a lot of places, this may result in a lot of extra code.
  • Add a parameter to SomeFunction to let it know whether it needs to lock the mutex or not.
  • Some kind of wrapper functions around lock/unlock, with a threaded variable to act as the counter. This would allow a thread to call WrapLockMutex() multiple times with only the first one really calling LockMutex(). And then, as long as it calls WrapUnlockMutex() the same number of times, it unlocks just once at the end.
But, if it already behaves as desired, I don't need to do any of that. Does anyone know for certain how this is handled?
#NULL
Addict
Addict
Posts: 1440
Joined: Thu Aug 30, 2007 11:54 pm
Location: right here

Re: Locking a mutex multiple times?

Post by #NULL »

It's not a problem, PB mutex locks are reentrant
https://en.wikipedia.org/wiki/Reentrant_mutex
User avatar
Dadido3
User
User
Posts: 52
Joined: Sat Jan 12, 2008 11:50 pm
Location: Hessen, Germany
Contact:

Re: Locking a mutex multiple times?

Post by Dadido3 »

Hi,

what i would do is to create variants of your functions. So one variant that locks/unlocks the mutex, and one that doesn't. To reduce duplicate code you can make the function that locks/unlocks the mutex a wrapper of the one that doesn't lock/unlock the mutex.

This works especially well, if you have your code that is protected by a single mutex encapsulated in a module. Then you can expose your functions that lock/unlock the mutex to the rest of the program, while you use the "internal" mutex-less functions inside the module. The result is very clean and readable code, as there is only a single "barrier" that causes locks/unlocks to happen, which is when you call module functions from the outside. Furthermore, this reduces the amount of calls to LockMutex and UnlockMutex, which reduces the overhead compared to counter based methods, or locking/unlocking mutexes several times per call.
mikejs wrote: Mon Nov 29, 2021 12:15 pm What would happen here is two calls to LockMutex() followed by two calls to UnlockMutex()
This is (or was) OS dependent. So some OS their mutex implementations use a thread based counter, so it was possible to "stack" multiple locks/unlocks. Some OS implementations don't do that, in which case you would cause a deadlock. I would not rely on that it uses a counter internally, especially as it's undefined in PB.

Edit:
#NULL wrote: Mon Nov 29, 2021 12:44 pm It's not a problem, PB mutex locks are reentrant
https://en.wikipedia.org/wiki/Reentrant_mutex
Interesting, i remember to have had problems on linux because they werent there. But that was years ago.
#NULL
Addict
Addict
Posts: 1440
Joined: Thu Aug 30, 2007 11:54 pm
Location: right here

Re: Locking a mutex multiple times?

Post by #NULL »

It seems to work here on my Ubuntu 18 / x86_64

Code: Select all

m = CreateMutex()
LockMutex(m)
LockMutex(m)
UnlockMutex(m)
UnlockMutex(m)
Debug "hi!"         ; hi!
User avatar
Dadido3
User
User
Posts: 52
Joined: Sat Jan 12, 2008 11:50 pm
Location: Hessen, Germany
Contact:

Re: Locking a mutex multiple times?

Post by Dadido3 »

The documentation needs to be updated in this case. It works here too, on Windows 11 x64 with PB 5.72.

But still, in my opinion it's bad practice to use mutexes in that way, as it can produce unreadable code. The result of unreadable code is that you may introduce mistakes, which cause unpredictable behavior. And the last thing you want to do is to debug multithreading problems with the PB debugger.
mikejs
Enthusiast
Enthusiast
Posts: 160
Joined: Thu Oct 21, 2010 9:46 pm

Re: Locking a mutex multiple times?

Post by mikejs »

Dadido3 wrote: Mon Nov 29, 2021 12:56 pm Hi,

what i would do is to create variants of your functions. So one variant that locks/unlocks the mutex, and one that doesn't. To reduce duplicate code you can make the function that locks/unlocks the mutex a wrapper of the one that doesn't lock/unlock the mutex.
Hi,

That's an interesting idea, but I can see ways in which that could bite in the future.

e.g. If I have a chunk of code that currently does not need the mutex, it would call the wrapped function that does the mutex handling. But, if that code is updated at some point in the future, and now does need the mutex, then I can't just top and tail it with lock and unlock, but have to go through the code and update the calls to the function to use it directly rather than the wrapped version. If I miss something when doing that, then we have the original problem again, of two locks, followed by two unlocks.

I'll have a bit more of a think about it though, as it is certainly neater than some of the other options.
mikejs
Enthusiast
Enthusiast
Posts: 160
Joined: Thu Oct 21, 2010 9:46 pm

Re: Locking a mutex multiple times?

Post by mikejs »

#NULL wrote: Mon Nov 29, 2021 1:04 pm It seems to work here on my Ubuntu 18 / x86_64

Code: Select all

m = CreateMutex()
LockMutex(m)
LockMutex(m)
UnlockMutex(m)
UnlockMutex(m)
Debug "hi!"         ; hi!
Hi,

This will be Windows x64, and the above works here too, but I'm not sure that gives enough info to go on.

What I was concerned about in my example was the idea that the mutex acts as a switch. i.e. calls to lockmutex switch it on (and have no effect if it is already on), and calls to unlockmutex switch it off (and have no effect if it is already off).

If that was how it worked, the mutex would unlock on the first call to unlockmutex, and then be unlocked for the remaining code (that needs the mutex protecting it), with potentially bad results. And the second call to unlockmutex would have no effect.

The other question (that I hadn't thought of) is the second call to lockmutex sticking because it's already locked. The fact that the above code works means we can rule that out, I think. But it doesn't resolve where in the above code the mutex actually unlocks.
#NULL
Addict
Addict
Posts: 1440
Joined: Thu Aug 30, 2007 11:54 pm
Location: right here

Re: Locking a mutex multiple times?

Post by #NULL »

You could add some test code (in debug mode) that uses a test thread which double-locks a mutex. If it doesn't finish after some time you give yourself a message. So when you port your code you will get notified immediately in case reentrant isn't working.
mikejs
Enthusiast
Enthusiast
Posts: 160
Joined: Thu Oct 21, 2010 9:46 pm

Re: Locking a mutex multiple times?

Post by mikejs »

Dadido3 wrote: Mon Nov 29, 2021 1:32 pm The documentation needs to be updated in this case. It works here too, on Windows 11 x64 with PB 5.72.

But still, in my opinion it's bad practice to use mutexes in that way, as it can produce unreadable code. The result of unreadable code is that you may introduce mistakes, which cause unpredictable behavior. And the last thing you want to do is to debug multithreading problems with the PB debugger.
Yep, that's fair enough. I would say though, that trying to avoid using mutexes in this way can also lead to unreadable code.

If the PB mechanism does work as a counter, it would mean that each place where the mutex is needed can do its own lock/unlock without having to worry about whether the caller of that function happened to have its own need for the mutex and is also doing its own lock/unlock. That keeps all the lock/unlocks as matched pairs in close proximity to where they are needed, which doesn't seem particularly unreadable to me.

And in any event, it would definitely be useful if the documentation gave some indication of what it actually does, and what is safe to do or not do with these calls.
mikejs
Enthusiast
Enthusiast
Posts: 160
Joined: Thu Oct 21, 2010 9:46 pm

Re: Locking a mutex multiple times?

Post by mikejs »

Hi,

I've done a quick test of how things actually work, and it seems, on windows at least, as if it is implemented in a reentrant manner. That is, it behaves as if it is keeping count of the calls to lock and unlock, and only unlocking when the count reaches zero.

Test code involving locks three levels deep:

Code: Select all

Global.i m = CreateMutex()

Procedure TestLock(param.l)
  Protected.l flag
  
  flag=#False
  Repeat
    If TryLockMutex(m)
      Debug Str(ElapsedMilliseconds())+" TestLock got mutex"
      flag=#True
    Else
      Debug Str(ElapsedMilliseconds())+" Still locked"
    EndIf
    Delay(100)
  Until flag
  
EndProcedure

LockMutex(m)
LockMutex(m)
LockMutex(m)

Global.i t = CreateThread(@TestLock(), 0)

Delay(50)
Debug Str(ElapsedMilliseconds())+" First unlock"
UnlockMutex(m)

Delay(100)
Debug Str(ElapsedMilliseconds())+" Second unlock"
UnlockMutex(m)

Delay(100)
Debug Str(ElapsedMilliseconds())+" Third unlock"
UnlockMutex(m)

Delay(100)
Debug Str(ElapsedMilliseconds())+" Done"
This produces debug output:

Code: Select all

0 Still locked
50 First unlock
100 Still locked
151 Second unlock
200 Still locked
251 Third unlock
300 TestLock got mutex
351 Done
i.e. the first and second calls to unlock has no immediate effect, and the mutex is only really unlocked (allowing TestLock() to claim it) after the final unlock.

I think that probably answers the question for now, and avoids the need for wrapper functions and so forth. It would definitely be useful if the documentation were explicit as to whether this should work or not (and if it is OS-dependent), and whether it is safe to rely on this behaviour.

Debugging this would also have been easier if there were a function to tell you whether your thread already has the mutex, without actually trying to claim it in the way that TryLockMutex() does.
User avatar
Caronte3D
Addict
Addict
Posts: 1027
Joined: Fri Jan 22, 2016 5:33 pm
Location: Some Universe

Re: Locking a mutex multiple times?

Post by Caronte3D »

mikejs wrote: Mon Nov 29, 2021 3:01 pm Debugging this would also have been easier if there were a function to tell you whether your thread already has the mutex, without actually trying to claim it in the way that TryLockMutex() does.
+1
Post Reply