arayq2
arayq2

Reputation: 2554

"terminate called without an active exception" after pthread_cancel

In probing the conditions of this question, a problem arose, exemplified by the code below.

#include <iostream>
#include <thread>
#include <chrono>
#include <stdexcept>
#include <cxxabi.h>

using namespace std;

// mocking external library call stuck in a strictly user-land infinite loop
int buggy_function_simulation()
{
    // cout << "In buggy function" << endl; // (1)
    int counter = 0;
    while (true)
    {
        if ( ++counter == 1000000 ) { counter = 0; }
    }
    return 0;
}

int main(int argc, char **argv) {
    cout << "Hello, world!" << endl;

    auto lambda = []() {
        pthread_setcanceltype( PTHREAD_CANCEL_ASYNCHRONOUS, nullptr );
        // cout << "ID: "<<pthread_self() <<endl; // (2)
        try
        {
            cout << "ID: "<<pthread_self() <<endl; // (3)
            buggy_function_simulation();
        }
        catch ( abi::__forced_unwind& )
        {
            cout << "thread cancelled!" << endl; // (4)
            throw;
        }
    };

    std::thread th(lambda);

    pthread_t id = th.native_handle();
    cout << id << endl;

    this_thread::sleep_for(chrono::seconds(1));
    cout << "cancelling ID: "<< id << endl;

    pthread_cancel(id);
    th.join();

    cout << "cancelled: "<< id << endl;

    return 0;
}

Compiling and running results in an abort:

$ g++ -g -Og -std=c++11 -pthread -o test test.cpp -lpthread
$ ./test
Hello, world!
139841296869120
ID: 139841296869120
cancelling ID: 139841296869120
terminate called without an active exception
Aborted (core dumped)
$

Note that the diagnostic output (4) does not appear.

If I comment out (3) and uncomment (2), the result is:

$ ./test
Hello, world!
139933357348608
ID: 139933357348608
cancelling ID: 139933357348608
cancelled: 139933357348608
$

Again, the output at (4) does not appear (why?), but the abort has been obviated.

If, alternately, I retain (3), leave (2) commented out, and uncomment (1), the result is finally as expected:

$ ./test
Hello, world!
139998901511936
ID: 139998901511936
In buggy function
cancelling ID: 139998901511936
thread cancelled!
cancelled: 139998901511936
$

So, the questions are:

For completeness, here is the stack trace from gdb for the first case:

Program terminated with signal SIGABRT, Aborted.
#0  __GI_raise (sig=sig@entry=6) at ../sysdeps/unix/sysv/linux/raise.c:51
51      ../sysdeps/unix/sysv/linux/raise.c: No such file or directory.
[Current thread is 1 (Thread 0x7f5d9b49a700 (LWP 12130))]
(gdb) where
#0  __GI_raise (sig=sig@entry=6) at ../sysdeps/unix/sysv/linux/raise.c:51
#1  0x00007f5d9b879801 in __GI_abort () at abort.c:79
#2  0x00007f5d9bece957 in ?? () from /usr/lib/x86_64-linux-gnu/libstdc++.so.6
#3  0x00007f5d9bed4ab6 in ?? () from /usr/lib/x86_64-linux-gnu/libstdc++.so.6
#4  0x00007f5d9bed4af1 in std::terminate() () from /usr/lib/x86_64-linux-gnu/libstdc++.so.6
#5  0x00007f5d9bed44ba in __gxx_personality_v0 () from /usr/lib/x86_64-linux-gnu/libstdc++.so.6
#6  0x00007f5d9bc3a708 in ?? () from /lib/x86_64-linux-gnu/libgcc_s.so.1
#7  0x00007f5d9bc3acfc in _Unwind_ForcedUnwind () from /lib/x86_64-linux-gnu/libgcc_s.so.1
#8  0x00007f5d9c1dbf10 in __GI___pthread_unwind (buf=<optimized out>) at unwind.c:121
#9  0x00007f5d9c1d0d42 in __do_cancel () at ./pthreadP.h:297
#10 sigcancel_handler (sig=<optimized out>, si=0x7f5d9b499bb0, ctx=<optimized out>) at nptl-init.c:215
#11 <signal handler called>
#12 buggy_function_simulation () at test.cpp:15
#13 0x0000558865838227 in <lambda()>::operator() (__closure=<optimized out>) at test.cpp:29
#14 std::__invoke_impl<void, main(int, char**)::<lambda()> > (__f=...) at /usr/include/c++/7/bits/invoke.h:60
#15 std::__invoke<main(int, char**)::<lambda()> > (__fn=...) at /usr/include/c++/7/bits/invoke.h:95
#16 std::thread::_Invoker<std::tuple<main(int, char**)::<lambda()> > >::_M_invoke<0> (this=<optimized out>)
    at /usr/include/c++/7/thread:234
#17 std::thread::_Invoker<std::tuple<main(int, char**)::<lambda()> > >::operator() (this=<optimized out>)
    at /usr/include/c++/7/thread:243
#18 std::thread::_State_impl<std::thread::_Invoker<std::tuple<main(int, char**)::<lambda()> > > >::_M_run(void) (
    this=<optimized out>) at /usr/include/c++/7/thread:186
#19 0x00007f5d9beff66f in ?? () from /usr/lib/x86_64-linux-gnu/libstdc++.so.6
#20 0x00007f5d9c1d26db in start_thread (arg=0x7f5d9b49a700) at pthread_create.c:463
#21 0x00007f5d9b95a88f in clone () at ../sysdeps/unix/sysv/linux/x86_64/clone.S:95

Upvotes: 8

Views: 3836

Answers (1)

Anthony Williams
Anthony Williams

Reputation: 68611

That message can be triggered if you throw from inside a function marked noexcept. All destructors are implicitly noexcept, so if the thread is running a destructor when the exception triggered by pthread_cancel is thrown, your program will terminate and you will get that message.

operator<< for std::cout is a formatted output operation, which constructs a sentry object, which is destructed on exit (see https://en.cppreference.com/w/cpp/named_req/FormattedOutputFunction). If the cancel comes while the destructor of the sentry object is being processed, this will thus terminate your application.

Do not use PTHREAD_CANCEL_ASYNCHRONOUS in C++. Even using pthread_cancel at all can be problematic due to the automatic rethrow from catch clauses.

UPDATE:

pthread_cancel is a POSIX C function, intended to work with C code. It has two modes of operation: synchronous and asynchronous.

Synchronous use of pthread_cancel sets an internal flag on the target thread which is then check in certain functions marked as cancellation points in the POSIX documentation. If any of those functions are called by the target thread, then cancellation is triggered. On Linux this is done by raising a special exception using the C++ exception mechanism that cannot be caught and discarded. This triggers stack unwinding, calls C++ destructors, and runs code registered with pthread_cleanup_push. This is compatible with normal C++ code, assuming nothing tries to catch and discard the exception. If all catch blocks rethrow, then everything works as expected. If the cancellation starts inside a function marked noexcept (such as a destructor, which is noexcept by default), then the program will terminate.

Asynchronous use of pthread_cancel is different. This sends a special signal to the target thread which interrupts it at any arbitrary point and starts the stack unwinding process described above. This is much more dangerous, as the code may be in the middle of evaluating any arbitrary expression, so the state of the application's data is much less well defined.

If you use asynchronous cancellation with code that has been designed to support it, then this can be OK. It may be possible to make code async-cancel-safe through careful use of pthread_setcancelstate to disabled cancellation in specific regions, and use of pthread_cleanup_push to register cancellation cleanup handlers, but this cannot be done in all cases.

With synchronous cancellation, if a function declared noexcept does not call any cancellation point functions, then all is well. With asynchronous cancellation, all code is a potential cancellation point, so before entering any code that is marked noexcept, you must call pthread_setcancelstate to temporarily disable cancellation, otherwise if the cancellation signal is received while that function is running then terminate will be called due to the cancellation exception. As noted above, this includes all destructors which are not explicitly marked noexcept(false).

Consequently, any call to arbitrary C++ library code (which may therefore construct C++ objects with destructors) is a potential hazard when using asynchronous cancellation, and you must call pthread_setcancelstate to disable cancellation around any block of code which creates C++ objects with destructors, and/or calls into C++ library code out of your control (such as standard library functions).

Upvotes: 11

Related Questions