2
$\begingroup$

I had a discussion with a C++ developer on whether aborting a thread causes memory leaks. His opinion on how aborting a thread works was

  1. all destructors of the stack objects of the thread to abort are executed by the C++ runtime
  2. the C++ runtime calls the OS method to abort the thread
  3. the kernel gets rid of the stack memory and the kernel thread object

AFAIK, that's not how it is done today, especially not step 1. And since C++ is focusing more on performance than on correctness, I'd say this will never happen.

But: would that even be feasible without changing to garbage-collection memory model?

$\endgroup$
3
  • 1
    $\begingroup$ There's no concept of "aborting a thread" in standard C++, at least not by that name. What precise C++ code are you talking about? $\endgroup$
    – benrg
    Dec 18, 2023 at 22:24
  • $\begingroup$ @benrg: using TerminateThread() and C++ native_handle() $\endgroup$ Dec 19, 2023 at 5:51
  • $\begingroup$ Threads will also be aborted when you call exit() or return from main. In that case destructors for static objects will be called - while they may still be in use by a thread. $\endgroup$
    – gnasher729
    Dec 19, 2023 at 11:13

2 Answers 2

2
$\begingroup$

You can abort a C++ thread in such a way that destructors of automatic ("stack") variables are called: just throw an exception that you don't catch (or that you catch only in a try block that wraps the thread's main function).

In a comment you said the question is about using TerminateThread. By design, TerminateThread stops thread execution immediately: "the target thread has no chance to execute any user-mode code", including destructors. It's safe to use only in very specialized circumstances. In general, a thread you terminate might, for instance, be in the middle of modifying a shared data structure while holding a lock. You can't solve this by having a background garbage collector notice the thread is gone and release the lock, because the shared data may be in a state that violates the lock invariant. The thread might even have been in the middle of acquiring or releasing the lock, leaving the GC with wrong information about the lock's state. It's not C++'s fault for "focusing more on performance than on correctness" (a claim I'd dispute in any case); it's simply impossible to guarantee correctness in the face of TerminateThread.

$\endgroup$
2
  • $\begingroup$ If C++ would focus on correctness, they would get rid of UB completely. $\endgroup$ Dec 19, 2023 at 9:01
  • $\begingroup$ @ThomasWeller Everything that C++ changes/adds relative to C is meant to make it easier to write correct code, so I'd say the committee is focused on correctness (while keeping an eye on performance). I'm not suggesting they couldn't have done a better job. $\endgroup$
    – benrg
    Dec 19, 2023 at 18:04
1
$\begingroup$

I agree, this is not how C++ handles thread abortion. I don't think such correctness guarantees can be achieved in C++ without turning to garbage collection (GC) techniques or binary instrumentation (BI) and changing the current semantics. C++ takes advantage of RAII, hence, it is the responsibility of the constructor to acquire and initialize resources, conversely, it's the duty of the destructor to undo such operations.

Generally speaking, the destructor of an object is executed at the end of the scope where it has been allocated or when the object is explicitly deleted. The former corresponds to additional code that invokes the destructor; it is placed at the end of the current scope and is filled in by the compiler.

The OS' abortion function is generally not graceful. Making it graceful in such a way that allocated objects are correctly freed is challenging. Notice it would require the help of the run-time execution support because the abortion should defer the invocation of the OS' primitive until all freeing has occurred.

Let's say we don't employ GC or BI, then it would require the execution of those pieces of code responsible for invoking the destructor, I think there are two possibilities, one applies at run-time, the other at compile-time:

  1. bookkeep ordered references to all allocated instances for each (possibly nested) scope; at abortion time delete all objects pointed by such references via their destructors (this will have problems with loopful data structures),
  2. rewire the execution flow in such a way that on abortion all code but the one responsible for invoking destructors is executed (impossible without some form of annotation).

The first proposal is a (badly) simplified version of GC. The second proposal is probably impossible to achieve in a general setting, it would require rewiring all functions of the program in such a way that when the abort function is invoked the program switches to an "abortion mode" that simply disables the execution of the business logic while allowing to execute destructors. I think it is technically possible to achieve a language behaving this way but it wouldn't be C++.

Conversely, let's say we are up to employ GC or BI.

On one hand, let's say we wan to employ GC. C++ is a non-cooperative language, hence making it work with a GC would challenging but the collector would be able to free those objects.

On the other hand, let's say we want to employ BI. This technique requires running the compiled program in a VM and switching to the abortion mode when the abort function is called. One open problem remains: How do we reliably distinguish the machine code required for freeing from business code?

Lastly, I think we can agree that it is far simpler and more effective to write the code in such a way that in case an error occurs we behave accordingly; either it IS critical, hence there is nothing to do, or it is not and there is still room for safely cleaning the environment.

$\endgroup$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.