Difference between revisions of "Threads"

From Lazarus wiki
Jump to navigationJump to search
m (→‎Notes on procedural thread management: Mention return values of CloseThread)
(More details on semaphores as of 3.2.0)
 
(3 intermediate revisions by 2 users not shown)
Line 1: Line 1:
 +
{{Threads}}
 +
 
Free Pascal supports thread programming, with a procedural and object-oriented interface that is mostly platform-neutral.
 
Free Pascal supports thread programming, with a procedural and object-oriented interface that is mostly platform-neutral.
  
Line 8: Line 10:
  
 
== Notes on procedural thread management ==
 
== Notes on procedural thread management ==
 +
  
 
<code>BeginThread</code> returns a thread handle directly, but also returns a thread ID as an output parameter. On Posix systems, there are no thread handles, so a copy of the thread ID is returned as both the ID and handle. On Windows, the thread handle is used for all thread management, while the ID is a separate unique value used as a thread enumerator. Therefore, regardless of platform, the thread handle returned by <code>BeginThread</code> is what all other RTL threading functions expect. (<code>CloseThread</code>, <code>SuspendThread</code>, <code>WaitForThreadTerminate</code>, etc)
 
<code>BeginThread</code> returns a thread handle directly, but also returns a thread ID as an output parameter. On Posix systems, there are no thread handles, so a copy of the thread ID is returned as both the ID and handle. On Windows, the thread handle is used for all thread management, while the ID is a separate unique value used as a thread enumerator. Therefore, regardless of platform, the thread handle returned by <code>BeginThread</code> is what all other RTL threading functions expect. (<code>CloseThread</code>, <code>SuspendThread</code>, <code>WaitForThreadTerminate</code>, etc)
Line 22: Line 25:
 
<code>DoneThread</code> and <code>InitThread</code> in the system unit are primarily for internal use, ignore them.
 
<code>DoneThread</code> and <code>InitThread</code> in the system unit are primarily for internal use, ignore them.
  
Avoid using <code>KillThread</code> if at all possible. If a thread happens to be blocking a manual synchronisation mechanism when it is terminated, any other thread waiting for that mechanism may be left waiting forever.
+
<code>KillThread</code> should be avoided if at all possible. If a thread happens to be blocking a manual synchronisation mechanism when it is terminated, any other thread waiting for that mechanism may be left waiting forever.
  
<code>SuspendThread</code> and <code>ResumeThread</code> are likewise dangerous, and not even supported on Posix systems, due to similar deadlocking concerns. Co-operative synchronisation is the safe alternative.
+
<code>SuspendThread</code> and <code>ResumeThread</code> are likewise dangerous, and not even supported on Posix systems, due to similar deadlocking concerns. Co-operative synchronisation is the safe alternative (threads themselves check periodically if they are being asked to shut down).
  
 
<code>ThreadGetPriority</code> and <code>ThreadSetPriority</code> allow setting a thread's priority between -15 (idle) and +15 (critical), on Windows. On Posix these functions are not implemented yet through the procedural interface.
 
<code>ThreadGetPriority</code> and <code>ThreadSetPriority</code> allow setting a thread's priority between -15 (idle) and +15 (critical), on Windows. On Posix these functions are not implemented yet through the procedural interface.
  
The system unit contains a [https://www.freepascal.org/docs-html/rtl/system/getcpucount.html GetCPUCount] function, which is a convenient way to decide at runtime how many threads to create.
+
There is a [https://www.freepascal.org/docs-html/rtl/system/getcpucount.html GetCPUCount] function in the system unit. Use this to estimate at runtime how many parallel threads to create.
 +
 
  
 
== Thread synchronisation ==
 
== Thread synchronisation ==
  
  
The native synchronisation mechanisms in the system unit are: critical sections, RTL events, semaphores, <code>WaitForThreadTerminate</code>.
+
Synchronisation is used to ensure different threads or processes access shared data in a safe manner. The most efficient synchronisation mechanisms are those provided by the operating system; FPC's synchronisation mechanisms act as a minimal platform-neutral wrapper around the operating system mechanisms.
 +
 
 +
The native thread synchronisation mechanisms in the system unit are: critical sections, RTL events, semaphores (deprecated), <code>WaitForThreadTerminate</code>. The code for these can be found in '''rtl/<arch>/cthreads.pp''' or '''rtl/<arch>/systhrd.inc'''.
  
 
Fully cross-process communication requires a more robust solution, as thread synchronisation mechanisms are insufficient. [https://www.freepascal.org/docs-html/fcl/simpleipc/index.html SimpleIPC] may be appropriate for this.
 
Fully cross-process communication requires a more robust solution, as thread synchronisation mechanisms are insufficient. [https://www.freepascal.org/docs-html/fcl/simpleipc/index.html SimpleIPC] may be appropriate for this.
Line 45: Line 51:
 
Critical sections are a co-operative code mutex, allowing only a single thread at a time into the protected code section, provided each thread enters and exits the section cleanly. This is a safe cross-platform way of protecting relatively small blocks of code.
 
Critical sections are a co-operative code mutex, allowing only a single thread at a time into the protected code section, provided each thread enters and exits the section cleanly. This is a safe cross-platform way of protecting relatively small blocks of code.
  
Threads are only blocked from the section when they call <code>EnterCriticalSection</code>. If a thread has an alternative way into the section, it is not blocked. Threads are released into the critical section in FIFO order.
+
Threads are only blocked from the section when they call <code>EnterCriticalSection</code>. If a thread is somehow able to get into the section without calling <code>EnterCriticalSection</code>, it is not blocked and the section remains unsafe. Threads are released into the critical section in FIFO order.
  
The critical section mutex has a lock counter. If a thread holding the critical section mutex calls <code>EnterCriticalSection</code> repeatedly, it will have to call <code>LeaveCriticalSection</code> an equal number of times to release the mutex. If a thread exits the section without calling <code>LeaveCriticalSection</code>, eg. due to an unhandled exception, the mutex remains locked and other threads waiting for it will be deadlocked.
+
The critical section mutex has a lock counter. If a thread holding the critical section mutex calls <code>EnterCriticalSection</code> repeatedly, it must call <code>LeaveCriticalSection</code> an equal number of times to release the mutex. If a thread exits the section without calling <code>LeaveCriticalSection</code>, eg. due to an unhandled exception, the mutex remains locked and other threads waiting for it will be deadlocked.
  
 
Calling <code>LeaveCriticalSection</code> when the mutex is not locked causes an error on Posix systems, but on Windows it decrements the lock counter to below 0. Calling <code>DoneCriticalSection</code> while the mutex is still in use causes an error on Posix systems, but on Windows it just deadlocks any threads waiting on the mutex, though any thread already in the critical section is able to leave normally.
 
Calling <code>LeaveCriticalSection</code> when the mutex is not locked causes an error on Posix systems, but on Windows it decrements the lock counter to below 0. Calling <code>DoneCriticalSection</code> while the mutex is still in use causes an error on Posix systems, but on Windows it just deadlocks any threads waiting on the mutex, though any thread already in the critical section is able to leave normally.
 +
  
 
=== RTLevent ===
 
=== RTLevent ===
 +
  
 
The functions used are <code>RTLEventCreate</code>, <code>RTLEventSetEvent</code>, <code>RTLEventResetEvent</code>, <code>RTLEventWaitFor</code>, and <code>RTLEventDestroy</code>.
 
The functions used are <code>RTLEventCreate</code>, <code>RTLEventSetEvent</code>, <code>RTLEventResetEvent</code>, <code>RTLEventWaitFor</code>, and <code>RTLEventDestroy</code>.
  
RTL events are the preferred cross-platform synchronisation method. They block threads waiting for them; when the event is set, a single waiting thread is released (in FIFO order) and the event is immediately reset.
+
RTL events are the preferred cross-platform synchronisation method. RTL events start out as unset. They block threads waiting for them; when the event is set, a single waiting thread is released (in FIFO order) and the event is immediately reset.
  
RTL events start out as unset, and internally retain a boolean set/unset state. There is no way to directly query the event's state. There is also no way to directly detect an event wait timeout, because the event is automatically reset and waits do not have a return value.
+
Due to platform differences, there is no way to directly query an event's current state. There is also no way to directly detect an event wait timeout, because any wait for the event causes an automatic state reset and waits do not have a return value.
  
 
If a thread starts to wait for an RTL event that has already been set, the wait completes immediately and the event is automatically reset. The event does not count how many times it has been set, so multiple sets are still cleared by a single reset. Be careful: if you have 8 threads starting to wait for a single RTL event, which will be set 8 times, threads may become deadlocked if anything clears multiple sets in one reset.
 
If a thread starts to wait for an RTL event that has already been set, the wait completes immediately and the event is automatically reset. The event does not count how many times it has been set, so multiple sets are still cleared by a single reset. Be careful: if you have 8 threads starting to wait for a single RTL event, which will be set 8 times, threads may become deadlocked if anything clears multiple sets in one reset.
  
Destroying an RTL event while a thread is waiting for it does not release the thread. It is the programmer's responsibility to ensure no thread is waiting for it when destroying the event.
+
Destroying an RTL event while a thread is waiting for it does not release the thread. It is the programmer's responsibility to ensure no thread is waiting for the event when the event is destroyed.
 +
 
  
 
=== Semaphores ===
 
=== Semaphores ===
  
  
Semaphores were Posix-only, and have been deprecated. They block threads waiting for them, and each <code>SemaphorePost</code> releases one waiting thread. Although semaphores are not available through the RTL, other implementations exist. (where?)
+
A semaphore blocks threads waiting for it, and each <code>SemaphorePost</code> releases one waiting thread.
 +
 
 +
The semaphore implementation in FPC's RTL was Posix-only, and has been deprecated, and removed as of FPC 3.2.0. If semaphores are to be reimplemented in the RTL, it must be compatible with Delphi's [http://docwiki.embarcadero.com/Libraries/Tokyo/en/System.SyncObjs.TSemaphore SyncObjs.TSemaphore]. Outside of the RTL, other custom implementations do exist, for example [https://forum.lazarus.freepascal.org/index.php?topic=48032.0 here].
  
  
Line 75: Line 86:
  
 
<code>WaitForSingleObject</code> and <code>WaitForMultipleObjects</code> famously do not exist under Linux, so they're not good for cross-platform use. They can be of some use under Windows.
 
<code>WaitForSingleObject</code> and <code>WaitForMultipleObjects</code> famously do not exist under Linux, so they're not good for cross-platform use. They can be of some use under Windows.
 
 
[[Category:Multitasking]]
 

Latest revision as of 12:38, 22 August 2020

English (en) polski (pl) русский (ru)

Free Pascal supports thread programming, with a procedural and object-oriented interface that is mostly platform-neutral.

The official documentation is here: Programmer's guide chapter 10. It describes the procedural threading interface thoroughly.

The object-oriented interface TThread is described on this more comprehensive page: Multithreaded Application Tutorial


Notes on procedural thread management

BeginThread returns a thread handle directly, but also returns a thread ID as an output parameter. On Posix systems, there are no thread handles, so a copy of the thread ID is returned as both the ID and handle. On Windows, the thread handle is used for all thread management, while the ID is a separate unique value used as a thread enumerator. Therefore, regardless of platform, the thread handle returned by BeginThread is what all other RTL threading functions expect. (CloseThread, SuspendThread, WaitForThreadTerminate, etc)

GetCurrentThreadID returns specifically the thread ID, not the handle. So, while you could use this on Posix systems to call RTL threading functions, this would fail on Windows. To get the handle, you could use GetCurrentHandle or OpenThread in the Windows API, or retain the handle you got from BeginThread.

There are three steps in a thread's lifespan:

  • Another thread creates it by calling BeginThread.
  • The thread completes its work, then exits its top level function; or ends itself explicitly by calling EndThread; or is killed by another thread by calling KillThread.
  • Another thread detects that the thread has terminated, and calls CloseThread to clean up.

CloseThread does nothing on Posix systems, but on Windows it releases the thread handle. If a program keeps creating new threads but neglects to close them, this is a resource leak and could lead to system instability in extreme cases. All handles are released when the program terminates, but it's still good practice to explicitly call CloseThread as soon as a thread exits. The return value of CloseThread is always 0 on Posix, but non-zero on Windows if successful.

DoneThread and InitThread in the system unit are primarily for internal use, ignore them.

KillThread should be avoided if at all possible. If a thread happens to be blocking a manual synchronisation mechanism when it is terminated, any other thread waiting for that mechanism may be left waiting forever.

SuspendThread and ResumeThread are likewise dangerous, and not even supported on Posix systems, due to similar deadlocking concerns. Co-operative synchronisation is the safe alternative (threads themselves check periodically if they are being asked to shut down).

ThreadGetPriority and ThreadSetPriority allow setting a thread's priority between -15 (idle) and +15 (critical), on Windows. On Posix these functions are not implemented yet through the procedural interface.

There is a GetCPUCount function in the system unit. Use this to estimate at runtime how many parallel threads to create.


Thread synchronisation

Synchronisation is used to ensure different threads or processes access shared data in a safe manner. The most efficient synchronisation mechanisms are those provided by the operating system; FPC's synchronisation mechanisms act as a minimal platform-neutral wrapper around the operating system mechanisms.

The native thread synchronisation mechanisms in the system unit are: critical sections, RTL events, semaphores (deprecated), WaitForThreadTerminate. The code for these can be found in rtl/<arch>/cthreads.pp or rtl/<arch>/systhrd.inc.

Fully cross-process communication requires a more robust solution, as thread synchronisation mechanisms are insufficient. SimpleIPC may be appropriate for this.


Critical sections

The functions used are InitCriticalSection, EnterCriticalSection, LeaveCriticalSection, and DoneCriticalSection.

Critical sections are a co-operative code mutex, allowing only a single thread at a time into the protected code section, provided each thread enters and exits the section cleanly. This is a safe cross-platform way of protecting relatively small blocks of code.

Threads are only blocked from the section when they call EnterCriticalSection. If a thread is somehow able to get into the section without calling EnterCriticalSection, it is not blocked and the section remains unsafe. Threads are released into the critical section in FIFO order.

The critical section mutex has a lock counter. If a thread holding the critical section mutex calls EnterCriticalSection repeatedly, it must call LeaveCriticalSection an equal number of times to release the mutex. If a thread exits the section without calling LeaveCriticalSection, eg. due to an unhandled exception, the mutex remains locked and other threads waiting for it will be deadlocked.

Calling LeaveCriticalSection when the mutex is not locked causes an error on Posix systems, but on Windows it decrements the lock counter to below 0. Calling DoneCriticalSection while the mutex is still in use causes an error on Posix systems, but on Windows it just deadlocks any threads waiting on the mutex, though any thread already in the critical section is able to leave normally.


RTLevent

The functions used are RTLEventCreate, RTLEventSetEvent, RTLEventResetEvent, RTLEventWaitFor, and RTLEventDestroy.

RTL events are the preferred cross-platform synchronisation method. RTL events start out as unset. They block threads waiting for them; when the event is set, a single waiting thread is released (in FIFO order) and the event is immediately reset.

Due to platform differences, there is no way to directly query an event's current state. There is also no way to directly detect an event wait timeout, because any wait for the event causes an automatic state reset and waits do not have a return value.

If a thread starts to wait for an RTL event that has already been set, the wait completes immediately and the event is automatically reset. The event does not count how many times it has been set, so multiple sets are still cleared by a single reset. Be careful: if you have 8 threads starting to wait for a single RTL event, which will be set 8 times, threads may become deadlocked if anything clears multiple sets in one reset.

Destroying an RTL event while a thread is waiting for it does not release the thread. It is the programmer's responsibility to ensure no thread is waiting for the event when the event is destroyed.


Semaphores

A semaphore blocks threads waiting for it, and each SemaphorePost releases one waiting thread.

The semaphore implementation in FPC's RTL was Posix-only, and has been deprecated, and removed as of FPC 3.2.0. If semaphores are to be reimplemented in the RTL, it must be compatible with Delphi's SyncObjs.TSemaphore. Outside of the RTL, other custom implementations do exist, for example here.


WaitForThreadTerminate

WaitForThreadTerminate blocks the current thread until the target thread has exited. The timeout parameter is ignored on Posix platforms, and the wait will never timeout.

WaitForSingleObject and WaitForMultipleObjects famously do not exist under Linux, so they're not good for cross-platform use. They can be of some use under Windows.