[4.30b3,4.20] Killing thread mutex trap with unlock key ;-)

Windows specific forum
Marlin
Enthusiast
Enthusiast
Posts: 406
Joined: Sun Sep 17, 2006 1:24 pm
Location: Germany

[4.30b3,4.20] Killing thread mutex trap with unlock key ;-)

Post by Marlin »

I played around with threads a bit and stumbled on something I almost considered a bug.

When a thread ends or is killed, it will not unlock any mutex it might have locked.

Code: Select all

Global myThread.l
Global myMutex.l

Procedure ThreadProc(*Dummy)
  ; ...
  LockMutex(myMutex)
  Delay(1000)
  UnlockMutex(myMutex)
  ;
EndProcedure

; try to get the mutex locked with timeout
Procedure GetMutexTm()
  Protected lngMaxRuns.l = 20  ; 20 x 100 ms = 2 secs.
  Protected lngRuns.l
  lngRuns = 0
  While Not TryLockMutex(myMutex)
    Delay(100)
    lngRuns + 1
    If lngRuns > lngMaxRuns
      Debug "Timeout waiting for mutex"
      ProcedureReturn #False
    EndIf
  Wend
  ProcedureReturn #True
EndProcedure


myMutex = CreateMutex()

myThread = CreateThread(@ThreadProc(), 1)

Delay (500)

KillThread(myThread)

Delay (200)
;UnlockMutex(myMutex)

If GetMutexTm()
  Debug "got the mutex locked :-)"
Else
  Debug "could not lock the mutex :-("
EndIf

Debug "The end."
As the thread is killed before it has any chance to unlock the mutex via explicit user code, the main program can not lock it for itself.

Seeing only that, I considered, that a thread should unlock all mutexes it has locked when it ends, either normally or by being killed.

However the mutex seems to work differently than I thought it would.

When you uncomment the line with "UnlockMutex(myMutex)" in the main progam, it can suddenly lock the mutex again.

So can any thread or the main program simply unlock any mutex, no matter if it was not locked by itself :?:

Is there any way to know, "who" locked a mutex?

When I kill a thread and want to enshure, it has not left any mutex locked, I could try to unlock it by the killing main.
But how then would I know if the mutex was not locked rightfully by some other thread?

Is this somehow covered by PureBasic, or should I simply build my own infrastuctur for that?
Marlin
Enthusiast
Enthusiast
Posts: 406
Joined: Sun Sep 17, 2006 1:24 pm
Location: Germany

Post by Marlin »

One simple way to handle that could be to first lock all the mutexes, the to be killed thread might have locked.

Then you kill it und unlock whatever you don't need to be locked any longer.

The trouble with that however ist, that you need all the threads to cooperate well.
That probably would not be the scenario when you're about to kill the thread,
as you could get it to end in a peacefull way then.

Any better solution?

Have I overlooked something?
Marlin
Enthusiast
Enthusiast
Posts: 406
Joined: Sun Sep 17, 2006 1:24 pm
Location: Germany

Post by Marlin »

The program probably knows who locked the mutex or this code would not run.

Code: Select all

Global myMutex.l
myMutex = CreateMutex()
Debug "start"
LockMutex(myMutex)
Debug "locked"
LockMutex(myMutex)
Debug "locked"
LockMutex(myMutex)
Debug "locked"
LockMutex(myMutex)
Debug "locked"
LockMutex(myMutex)
Debug "locked"
LockMutex(myMutex)
Debug "locked"
LockMutex(myMutex)
Debug "locked"
UnlockMutex(myMutex)
Debug "the end"
User avatar
Demivec
Addict
Addict
Posts: 4282
Joined: Mon Jul 25, 2005 3:51 pm
Location: Utah, USA

Post by Demivec »

Marlin wrote:The program probably knows who locked the mutex or this code would not run.
PB manual - LockMutex()) wrote:Waits until the mutex object is available (not locked by another thread) and then locks the object so no other thread can get a lock on the object.
freak
PureBasic Team
PureBasic Team
Posts: 5948
Joined: Fri Apr 25, 2003 5:21 pm
Location: Germany

Post by freak »

Better avoid KillThread() alltogether. It is a dangerous command.
quidquid Latine dictum sit altum videtur
User avatar
Rescator
Addict
Addict
Posts: 1769
Joined: Sat Feb 19, 2005 5:05 pm
Location: Norway

Post by Rescator »

Freak is right.
In recent software (and new versions of my old) I've begun to instead signal
a thread to die gracefully.

One way to do this is to make a single instance thread, the easiest type of thread.
Like a audio thread, or graphics thread, or download thread, or simple file processing thread. Use a static variable in the procedure.
These threads either exist during the whole run of the program, or are used then removed. There are never two of the same type of thread.

Another is the multiple instance threads.
These are threads that do exactly the same but multiple times,
like two different file processing threads, or parallel calculations,
server instances etc.
Usually the number of threads are balanced to a multiple of the number of cores and so on.

Below are two implementations on how to gracefully make a single instance or multiple instance thread properly quit.

The single instance thread is only useful for that specific purpose, trying to use that for multiple threads will cause race conditions, and impossible to locate bugs due to multiple threads messing with the static running variable.

The multiple instance is a tad more complex but not much, and it can obviously be used for single instance threads as well and have the added benefit of being able to pass more flags/states in the thread structure which is very cool.

The code can probably be improved further by making some nice macros to simplify some of the creation and quitting of the multiple instance example.

Code: Select all

EnableExplicit

;Example 1:
Procedure.i ThreadSingleInstance_proc(start.i=#True)
 Static running.i=#False
 If start
  If Not running
   running=#True
   While running
    ;The thread loop
    Delay(1)
   Wend
   ;clean stuff up, and let the proceduee/thread end naturally,
   ;PB should clean up allocations nicely this way too.
   Debug "ending single"
  Else
   ;We are allready running, ignoring.
  EndIf
 Else
  running=#False
 EndIf
EndProcedure

;Start the thread:
If CreateThread(@ThreadSingleInstance_proc(),#True)

 ;Let's do some other stuff here.
 Delay(1000) ;Hey, I'm too lazy to do anything fancy here.

 ;End the thread:
 ThreadSingleInstance_proc(#False)
EndIf

;-------------------------------------------------------------------

;Example 2:
Structure ThreadMultipleInstance_struct
 running.i
 ;You can add further state vars here for the thread if you need them.
 ;Just make sure you avoid race conflicts though.
 ;What to put here? Well, a mutex var would be great here for example or something else, like a pause var for when the program is minimized etc.
EndStructure

Procedure.i ThreadMultipleInstance_proc(*thread.ThreadMultipleInstance_struct)
 If *thread
  If Not *thread\running
   *thread\running=#True
   While *thread\running
    ;The thread loop
    Delay(1)
   Wend
   ;clean stuff up, and let the proceduee/thread end naturally,
   ;PB should clean up allocations nicely this way too.
   ;You could even free the structure memory "here" by doing FreeMemory(*thread)
   Debug "ending multi"
  Else
   ;We are allready running, ignoring.
  EndIf
 Else
  ;thread structure pointer was 0 for some odd reason, ignore.
 EndIf
EndProcedure

;Start the thread:
Define *thread1.ThreadMultipleInstance_struct
*thread1=AllocateMemory(SizeOf(ThreadMultipleInstance_struct)) ;Some of this stuff could probably be put in a macro or creation procedure.
If *thread1
 If CreateThread(@ThreadMultipleInstance_proc(),*thread1)

  ;Let's do some other stuff here.
  Delay(1000) ;Hey, I'm too lazy to do anything fancy here.

 EndIf
 ;End the thread:
 *thread1\running=#False
 Delay(100) ;To make sure the thread manages to read it before we free it.
 FreeMemory(*thread1) ;To avoid a racing issue here you might want to free the structure in the thread itself instead.
EndIf

Delay(1000) ;give stuff time to end above.
Marlin
Enthusiast
Enthusiast
Posts: 406
Joined: Sun Sep 17, 2006 1:24 pm
Location: Germany

Post by Marlin »

@Rescator: yes this pointer is a usefull way to exchange status information with threads.

There was more to the mutex object, than I thought.

I used some testing code like that:

Code: Select all

Global myThread.l

Global myMutex.l

Enumeration
  #mniRunThread
  #mniLockMutex
  #mniUnLockMutex
  #mniStopThread
EndEnumeration

Procedure TestThread(*Dummy)
  Protected N.q
  Debug "Thread started."
  Repeat
    Delay(1500)
    If TryLockMutex(myMutex)
      Debug "mutex was unlocked."
      UnlockMutex(myMutex)
    Else
      Debug "mutex was locked."
      N = 0
      Repeat
        UnlockMutex(myMutex)
        N + 1
      Until TryLockMutex(myMutex)
      Debug Str(N) + " locks removed."
      UnlockMutex(myMutex)
    EndIf
    If GetMenuItemState(0, #mniStopThread)
      SetMenuItemState(0, #mniStopThread, 0)
      Break
    EndIf
  ForEver
  Debug "Thread ended."
EndProcedure

Procedure RunThread()
  myThread = CreateThread(@TestThread(), 1)
EndProcedure


myMutex = CreateMutex()

OpenWindow(0, 500, 100, 300, 300, "TestThread")
CreateGadgetList(WindowID(0))

CreateMenu(0, WindowID(0))
MenuTitle("Action")
  MenuItem(#mniRunThread, "RunThread")
  MenuBar()
  MenuItem(#mniLockMutex, "Lock Mutex")
  MenuItem(#mniUnLockMutex, "Unlock Mutex")
  MenuBar()
  MenuItem(#mniStopThread, "Stop Thread")

Repeat
  Event.l = WaitWindowEvent()
  Select Event
    Case #PB_Event_Menu
      Select EventMenu()
        Case #mniRunThread
          RunThread()
        Case #mniLockMutex
          LockMutex(myMutex)
        Case #mniUnLockMutex
          UnlockMutex(myMutex)
        Case #mniStopThread
          SetMenuItemState(0, #mniStopThread, 1)
      EndSelect
  EndSelect
Until Event = #PB_Event_CloseWindow

CloseWindow(0)
FreeMutex(myMutex)

End
It seems you can lock it more than once and in that case you also need to unlock it as many times as you locked it.

That is usefull when using sub procedures that lock and unlock the mutex...

One thing that turned out to be deadly, was to unlock the mutex when it was not locked.
You can do that, but after that you will never be able to lock it again.

Very unfortunately this does not seem to behave the same way with linux.

When I lock the mutex there and try to lock it again the program just hangs.
It seems the program can not lock even the mutex it has locked for itself.

Is there a technical reason, why this would be different for linux than for windows?
Post Reply