Date: Fri, 14 Jul 2023 16:35:35 +0200
Hi Юрий,
if you want to stay within the standard, want the resources to be freed cleanly and stay within the C++ exception semantics, you have to accept potential problems during thread termination:
To free arbitrary resources, code (inside the destructors) is run. That code can deadlock, too. How would you react?
The destructors could theoretically throw another exception
C++ code normally knows, whether exceptions can happen, e.g. not in noexcept expressions. How and where would the exception throwing be inserted into the program? An interrupt handler does not know the current state and instructions of the program.
In the end those threads probably would have to be compiled in a mode similar to a debug build, guaranteeing that the thread can be interrupted at any time (as if the user presses the pause button of the debugger).
Best,
Sebastian
-----Ursprüngliche Nachricht-----
Von:Юрий Петренко via Std-Proposals <std-proposals_at_[hidden]>
Gesendet:Fr 14.07.2023 15:44
Betreff:[std-proposals] Forced stopping of RAII compliant execution threads.
An:std-proposals_at_[hidden];
CC:Юрий Петренко <petrenkoyura1981_at_[hidden]>;
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.
--
Std-Proposals mailing list
Std-Proposals_at_[hidden]
https://lists.isocpp.org/mailman/listinfo.cgi/std-proposals
Received on 2023-07-14 14:35:37