Date: Fri, 14 Jul 2023 16:43:50 +0300
*Abstract.*
The subject is the problem of forcing a thread to stop externally, similar
to using TerminateThread, without leaking resources, such as memory
allocated on a heap. Two methods are considered, the first involves changes
in the C++ standard, the second offers a solution within the existing
standard. It should be noted that the second solution is less flexible.
*Keywords.*
C++, thread, RAII, TerminateThread
*Introduction.*
An execution thread is the smallest unit of processing, the execution of
which can be assigned by the operating system kernel. Multiple execution
threads can exist within the same process and share resources such as
memory.
When using multi-threading, one of the challenges facing the programmer is
to arrange for thread stopping from the outside.
There are currently two ways to stop a thread in C++:
1. Requesting a thread to stop, e. g. using a stop token, but the thread
stop will be delayed. It is up to the running thread to decide at what
stages to check the stop token and at what frequency, and whether to check
it at all. The advantage of this method is that the thread will have the
time it needs to release all of its resources. The downside is that in the
case of a deadlock, this check will never happen. Also, because the thread
decides itself when to check the stop token, this method does not allow the
thread to terminate immediately, which can be a critical disadvantage if
the thread corrupts some important data.
2. Forced termination of a thread, i. e. to terminate it with
TerminateThread, in which case the thread is just halted, the stack is
destroyed, and dynamic resources are leaking. The advantage of this method
is that it allows you to kill a blocked thread and the thread is terminated
immediately. The disadvantage is that resources will leak.
Both of these methods do not provide an ideal method of stopping a thread
from the outside. Ideally, this method should stop the thread at an
arbitrary point in time and release all resources correctly. RAII compliant
execution threads allow us to come close to this ideal.
*Discussion.*
RAII is a concept according to which "getting a resource is an
initialisation". This idiom can be used to automate the release of
dynamically allocated resources. To do this, the resource in use is wrapped
in a wrapper class whose constructor allocates the resource and whose
destructor releases it. A necessary condition is that the wrapper class
destructor must be guaranteed to be called during stack unwinding.
If all the resources owned by a thread meet the requirements of RAII, we
will call that thread a RAII thread.
This will give us a guarantee that all resources owned by the thread will
be correctly released during the stack unwind process, regardless of the
reason. Thus, if the execution thread is a RAII thread, we can stop it
instantly externally, unwind the stack, freeing all resources and then
safely destroy it. This can be thought of as generating an exception
externally in a thread being stopped at its execution point.
Implementing this functionality in practice requires changes to the thread
manager and thread handler.
One implementation can be based on the following principle.
We need two execution thread handlers: the first one is a standard handler,
which we will call the "execution handler", its task is to ensure execution
of the thread, the second one is an "interrupt handler", its task is to
unwind the stack. When thread manager receives command to stop a thread, it
replaces the execution handler with an interrupt handler and starts
unwinding stack. At the same time, a pointer to the stack top and program
execution point must be passed to the interrupt handler. A request to stop
the thread can come at the moment of execution of the prologue or epilogue
of a function, which will make it difficult to unwind the stack. It is
reasonable to let the prologue or epilogue to finish its work and only
after that to start unwinding the stack. The execution handler should set
or clear the flag telling the thread that we have entered or exited the
prologue or epilogue, while the interrupt handler should analyze that flag
and if the flag is set, execute the program before exiting the prologue or
epilogue and, when it gets into the function body, interrupt the program
and unwind the stack.
The request to stop may come when a thread is modifying a global object,
which may be left in an invalid state, affecting the work of all the other
threads. This problem can be solved by a transactional approach to handling
such objects, or by adding a flag signaling that a critical part of the
work is happening and should preferably be allowed to terminate. An
interrupt handler can ignore such a flag if the stop request is at an
elevated level.
The current standard (C++ 20) does not provide any abilities to execute the
described algorithm with existing language means.
Within the current standard, a different approach is possible.
It is necessary to create a registry to store copies of RAII wrappers that
refer to the allocated resources. This registry should be located outside
the thread's memory and should exist throughout the lifecycle of the
thread. When a resource is allocated, a copy of the RAII shell is placed in
the registry; when the resource is released, the copy is removed from the
registry. Thus, even if TerminateThread is applied to the thread, we still
have the shell registry, which can be cleared even after the thread is
destroyed.
This method is an upgrade of the technique that offers to allocate dynamic
memory to a thread in advance and manage the lifetime of that block
externally. The thread can take the memory from the allocated block as
required. When the thread finishes, the memory block is released. Thus,
when terminating a thread using TerminateThread, it is sufficient to free
the entire block of dynamic memory owned by the thread. This technique
ensures that no memory leaks from the thread in case of a thread's
emergency termination, but it has some drawbacks. This method is an
inefficient use of memory; the thread keeps memory which it does not use.
In addition, the memory available to the thread is limited to an allocated
block. Destructors of dynamic objects will not be called. Therefore, using
the registry with RAII shells seems to be the preferred method.
RAII thread provides additional options also when requesting to stop the
thread via a stop token. In this case, it is possible to stop the thread
safely by generating an exception at the moment the stop request is
detected. Instead of organizing the logic for stopping the thread
prematurely, we just unwind the stack using standard means. Requiring a
thread to stop prematurely is inherently exceptional and using an exception
to stop a thread is not against C++ ideology. Sometimes this arrangement of
thread termination allows us to simplify the code.
An additional bonus is that a RAII thread which stops on request via a stop
token with exception generation can be used in the future to force a thread
to stop externally via stack unwinding (which cannot be organized yet due
to language limitations) without any code changes. Checking token and
throwing exception can be done by macro, and then it will be easy to remove
it if needed.
*Conclusion:*
An execution thread meeting RAII requirements principally allows to stop
itself forcibly from the outside like TerminateThread and to avoid memory
leaks. It is possible to arrange a safe, from the point of view of avoiding
memory leaks, forced stop of a thread with the existing C++ means. It is
also possible to control stopping of RAII threads in a more flexible way,
but it requires modification of the C++ standard to implement the
principles described in this article.
For RAII threads it is acceptable to stop on demand using stop-token
through exception generation.
The subject is the problem of forcing a thread to stop externally, similar
to using TerminateThread, without leaking resources, such as memory
allocated on a heap. Two methods are considered, the first involves changes
in the C++ standard, the second offers a solution within the existing
standard. It should be noted that the second solution is less flexible.
*Keywords.*
C++, thread, RAII, TerminateThread
*Introduction.*
An execution thread is the smallest unit of processing, the execution of
which can be assigned by the operating system kernel. Multiple execution
threads can exist within the same process and share resources such as
memory.
When using multi-threading, one of the challenges facing the programmer is
to arrange for thread stopping from the outside.
There are currently two ways to stop a thread in C++:
1. Requesting a thread to stop, e. g. using a stop token, but the thread
stop will be delayed. It is up to the running thread to decide at what
stages to check the stop token and at what frequency, and whether to check
it at all. The advantage of this method is that the thread will have the
time it needs to release all of its resources. The downside is that in the
case of a deadlock, this check will never happen. Also, because the thread
decides itself when to check the stop token, this method does not allow the
thread to terminate immediately, which can be a critical disadvantage if
the thread corrupts some important data.
2. Forced termination of a thread, i. e. to terminate it with
TerminateThread, in which case the thread is just halted, the stack is
destroyed, and dynamic resources are leaking. The advantage of this method
is that it allows you to kill a blocked thread and the thread is terminated
immediately. The disadvantage is that resources will leak.
Both of these methods do not provide an ideal method of stopping a thread
from the outside. Ideally, this method should stop the thread at an
arbitrary point in time and release all resources correctly. RAII compliant
execution threads allow us to come close to this ideal.
*Discussion.*
RAII is a concept according to which "getting a resource is an
initialisation". This idiom can be used to automate the release of
dynamically allocated resources. To do this, the resource in use is wrapped
in a wrapper class whose constructor allocates the resource and whose
destructor releases it. A necessary condition is that the wrapper class
destructor must be guaranteed to be called during stack unwinding.
If all the resources owned by a thread meet the requirements of RAII, we
will call that thread a RAII thread.
This will give us a guarantee that all resources owned by the thread will
be correctly released during the stack unwind process, regardless of the
reason. Thus, if the execution thread is a RAII thread, we can stop it
instantly externally, unwind the stack, freeing all resources and then
safely destroy it. This can be thought of as generating an exception
externally in a thread being stopped at its execution point.
Implementing this functionality in practice requires changes to the thread
manager and thread handler.
One implementation can be based on the following principle.
We need two execution thread handlers: the first one is a standard handler,
which we will call the "execution handler", its task is to ensure execution
of the thread, the second one is an "interrupt handler", its task is to
unwind the stack. When thread manager receives command to stop a thread, it
replaces the execution handler with an interrupt handler and starts
unwinding stack. At the same time, a pointer to the stack top and program
execution point must be passed to the interrupt handler. A request to stop
the thread can come at the moment of execution of the prologue or epilogue
of a function, which will make it difficult to unwind the stack. It is
reasonable to let the prologue or epilogue to finish its work and only
after that to start unwinding the stack. The execution handler should set
or clear the flag telling the thread that we have entered or exited the
prologue or epilogue, while the interrupt handler should analyze that flag
and if the flag is set, execute the program before exiting the prologue or
epilogue and, when it gets into the function body, interrupt the program
and unwind the stack.
The request to stop may come when a thread is modifying a global object,
which may be left in an invalid state, affecting the work of all the other
threads. This problem can be solved by a transactional approach to handling
such objects, or by adding a flag signaling that a critical part of the
work is happening and should preferably be allowed to terminate. An
interrupt handler can ignore such a flag if the stop request is at an
elevated level.
The current standard (C++ 20) does not provide any abilities to execute the
described algorithm with existing language means.
Within the current standard, a different approach is possible.
It is necessary to create a registry to store copies of RAII wrappers that
refer to the allocated resources. This registry should be located outside
the thread's memory and should exist throughout the lifecycle of the
thread. When a resource is allocated, a copy of the RAII shell is placed in
the registry; when the resource is released, the copy is removed from the
registry. Thus, even if TerminateThread is applied to the thread, we still
have the shell registry, which can be cleared even after the thread is
destroyed.
This method is an upgrade of the technique that offers to allocate dynamic
memory to a thread in advance and manage the lifetime of that block
externally. The thread can take the memory from the allocated block as
required. When the thread finishes, the memory block is released. Thus,
when terminating a thread using TerminateThread, it is sufficient to free
the entire block of dynamic memory owned by the thread. This technique
ensures that no memory leaks from the thread in case of a thread's
emergency termination, but it has some drawbacks. This method is an
inefficient use of memory; the thread keeps memory which it does not use.
In addition, the memory available to the thread is limited to an allocated
block. Destructors of dynamic objects will not be called. Therefore, using
the registry with RAII shells seems to be the preferred method.
RAII thread provides additional options also when requesting to stop the
thread via a stop token. In this case, it is possible to stop the thread
safely by generating an exception at the moment the stop request is
detected. Instead of organizing the logic for stopping the thread
prematurely, we just unwind the stack using standard means. Requiring a
thread to stop prematurely is inherently exceptional and using an exception
to stop a thread is not against C++ ideology. Sometimes this arrangement of
thread termination allows us to simplify the code.
An additional bonus is that a RAII thread which stops on request via a stop
token with exception generation can be used in the future to force a thread
to stop externally via stack unwinding (which cannot be organized yet due
to language limitations) without any code changes. Checking token and
throwing exception can be done by macro, and then it will be easy to remove
it if needed.
*Conclusion:*
An execution thread meeting RAII requirements principally allows to stop
itself forcibly from the outside like TerminateThread and to avoid memory
leaks. It is possible to arrange a safe, from the point of view of avoiding
memory leaks, forced stop of a thread with the existing C++ means. It is
also possible to control stopping of RAII threads in a more flexible way,
but it requires modification of the C++ standard to implement the
principles described in this article.
For RAII threads it is acceptable to stop on demand using stop-token
through exception generation.
Received on 2023-07-14 13:44:04