Thread Cleanup
A thread is a system object and as such it has to be
destroyed. An object has to perform cleanup upon destruction.
It is possible to terminate an application without performing
object cleanup for C++ objects but a thread is a system object and therefore the
system does some of the cleaning for us even if the application had been
terminated. A simple example is releasing a MUTEX object that was owned by the
thread.
There are still things that the operating system will not
cleanup for the thread and these are usually related to logic that is found in
our code. For example only our code knows when it is allowed to delete a buffer
in memory and that it is no longer used, and only our code knows the appropriate
C++ destructor for an object.
It is good practice to perform all cleanup in our code at all
times and not to let the system do the cleanup for us.
Here is a list of considerations for thread cleanup:
Free Memory
Data and buffers on the stack belong to the thread and are
'erased' when the thread exits. Typically destructors are called for these
objects as the thread steps out of scopes to exit.
Buffers that were allocated on the Heap are a different
story. These buffers has no owner thread and can be shared between threads of
the same process. It is possible that one thread allocated buffers for another
thread and then the first thread exits. Only the logic in our code can decide
who is the owner thread of the buffer and only the owner is allowed to
deallocate the buffer.
A thread that is the owner of a buffer is usually the last
thread to have the pointer to that buffer. This thread has to delete the buffer
on exit or the buffer will stay in memory with no owner and the application will
have a Memory Leak.
Every thread must free the memory buffers that it owns.
Close Handles
System handles are also shared between threads of the same
process. Just like memory buffers, system handles have an owner thread that is
determined by the logic in our code.
The owner thread is usually the last thread to have the valid
Handle to the system object. This thread must close the Handle by calling the
appropriate system API. If the owner thread exits without performing this
cleanup then the application will keep owning the system resource but there is
no way to free it because the value of the Handle is lost. This application has
a Resource Leak.
Every thread must close all the system Handles that it owns.
Stop waiting threads
In many designs and design patterns for parallel computing
systems there are threads that we use for performing Wait operations. Examples
are a thread waiting for communication, a thread waiting for an operation to
complete, etc. These threads are paused in a Wait State or looping on Wait.
Common design is that there is a thread that is waiting on a resource as its
owner and all other threads are serviced by this thread. When a thread
terminates it has to notify the waiting thread that it does not need the data
any more. If it does not tell the waiting thread that the data is no longer
required, the thread might keep waiting and process the unnecessary data and
waste processing power. The waiter thread could start a reaction as required
that might cause other threads to activate and run other tasks. Sometimes such a
thread will relay the data and then go back to enter the wait state.
A thread must stop all thread waiting in its behalf and must
notify all other threads that it canceled all of its operations.
If a thread has created another thread and is its owner then
it must also close the owned thread and allow it to perform proper cleanup.
Queue
Most Tasks and threads use input queues. These reduce Wait
times and allow better management of resources in comparison to locking on a
resource and using it based on thread priority instead of event priority. An
input queue may prioritize different events by the type or the data contained by
going over the queued items and peeking at the data.
When a thread exits it must attend to the queue. It is
possible that there are important messages there. A simple example is a storage
queue. The application might use an internal disk queue to improve write
performance. When the application exits all threads will try to save settings to
the disk and they do that by posting the data to the queue and continue with
their cleanup. If the disk queue thread starts performing the cleanup when the
command to exit was issued then it will destroy the queue and all other
resources. This thread should postpone the cleanup until it had emptied its
queue. It is possible that the application will tell the thread to exit after
all other threads report that they finished their cleanup but even then it is
possible that the settings to write to the disk are still in queue and were not
even processed.
A thread must attend to its queue before closing and it is
the design that should specify when the thread is allowed to start with the
cleanup sequence.
Cancel Callbacks
Many times threads use callbacks or APCs to be notified that
something happened, instead of waiting for many events. The thread has to
register to the callback with a Library or with another thread.
On cleanup the thread that registered must cancel the
registration or the library will call the callback function after the thread has
cleaned up the data, which may result in access to uninitialized data and
pointers. Some times canceling the callback will stop a thread that is waiting
in the background. It is possible that canceling a callback will send a response
such as calling the callback with a 'user abort' parameter. In such cases the
callback should be called before starting the relevant cleanup and the exiting
thread should stall destruction until the stage of the callback cleanup is
complete. This is to be defined in application design.
Cancel I/O
Threads wait on system resources. When the thread stops it
will also stop waiting on the resource. When the thread is waiting on an
operation for example writing to disk, it should also cancel this operation. I/O
operations are almost always slower than working with non-I/O resources. This
means that long after the thread exited there are still pending I/O operations.
For example if your thread writes 2GB to a Flash device it would take a few
minutes. The thread can send all the buffers to the system below and go back to
work. The system will send the buffers one by one to the Flash drive. If the
thread decided that the operation is no longer important and exits then the
write operation should be canceled.
Any I/O that the thread initiated and manages has to be
canceled before the thread exits as part of the thread's cleanup.
Complete Communication
Communication has many layers. Some of these layers are
managed by the system for us. For example a TCP socket will manage the Hand
Shake sequence for us where as UDP will only manage the layer below that. The
majority of communication systems have an internal communication state above the
one that the system implements. An example is the Session that browsers keep on
top of the TCP layer. If a browser exits without telling the host that it did
then the session will stay open. If only one browser is allowed per user and the
user is active on the dead session then the system will lock that user out.
A thread must complete the communications before exiting.
Usually this means notifying the other side that the communication ends. Most
times this includes completing a sequence. For example sending the rest of the
document to the printer or sending the rest of the rest of the 'raw data' buffer
to the other side so that the control 'end communication' can be sent.
Current Task
Threads should perform cleanup as soon as possible and exit
when the command to exit was given. On the other hand the thread should complete
its current task unless a Cancel Operation command was given to it. If the
thread is in the middle of a file update then it has to complete this task or
the file might be corrupt. If the thread is updating a buffer then it has to
finish this update. Some tasks include sequences of operations for example a
thread that should delete a file, copy a file, rename it, and mark it as hidden.
If this operation terminates during the flow then instead of a hidden file
called 'settings.dat' that the application uses we will get a visible file
called '~settings.tmp' and the application will reset to defaults on the next
run and lose all user information.
Threads should always complete current tasks before exiting.