关于C#:pthread_cancel之后的”无活动异常的终止调用”

“terminate called without an active exception” after pthread_cancel

在探究这个问题的条件时,出现了一个问题,例如下面的代码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
#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;
}

编译并运行导致中止:

1
2
3
4
5
6
7
8
9
$ 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)
$

请注意,不会出现诊断输出(4)。

如果我注释掉(3)并取消注释(2),结果是:

1
2
3
4
5
6
7
$ ./test
Hello, world!
139933357348608
ID: 139933357348608
cancelling ID: 139933357348608
cancelled: 139933357348608
$

同样,(4)的输出未出现(为什么?),但已取消中止操作。

或者,如果我保留(3),将(2)注释掉,然后取消注释(1),则结果最终符合预期:

1
2
3
4
5
6
7
8
9
$ ./test
Hello, world!
139998901511936
ID: 139998901511936
In buggy function
cancelling ID: 139998901511936
thread cancelled!
cancelled: 139998901511936
$

因此,问题是:

  • 在第一种情况下,"在没有活动异常的情况下终止调用"的原因是什么?
  • 为什么在第二种情况下未激活挡块?
  • 为什么在第三种情况下不加注释(1)会有如此不同?

出于完整性考虑,以下是第一种情况下gdb的堆栈跟踪:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
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


如果从标记为noexcept的函数内部抛出该消息,则可以触发该消息。所有析构函数都隐式地为noexcept,因此,如果在引发pthread_cancel触发的异常时线程正在运行析构函数,则程序将终止并得到该消息。

std::cout

operator<<是格式化的输出操作,它构造一个sentry对象,该对象在退出时被破坏(请参阅https://en.cppreference.com/w/cpp/named_req/FormattedOutputFunction)。如果在处理sentry对象的析构函数的同时取消,则将终止您的应用程序。

请勿在C中使用PTHREAD_CANCEL_ASYNCHRONOUS。由于从catch子句自动重新抛出,甚至完全使用pthread_cancel都可能会出现问题。

更新:

pthread_cancel是POSIX C函数,旨在与C代码一起使用。它具有两种操作模式:同步和异步。

pthread_cancel的同步使用在目标线程上设置一个内部标志,然后在POSIX文档中检入标记为取消点的某些功能。如果目标线程调用了这些函数中的任何一个,则会触发取消。在Linux上,这是通过使用不能捕获和丢弃的C异常机制引发特殊异常来完成的。这将触发堆栈展开,调用C析构函数并运行在pthread_cleanup_push中注册的代码。假定没有尝试捕获和丢弃异常的情况,这与常规C代码兼容。如果所有捕获块都重新抛出,那么一切都会按预期进行。如果取消开始于标记为noexcept的函数内(例如析构函数,默认情况下为noexcept),则程序将终止。

pthread_cancel的异步使用是不同的。这会向目标线程发送一个特殊信号,该信号会在任意任意点中断目标线程,并开始上述堆栈展开过程。这要危险得多,因为代码可能处于评估任意表达式的中间,因此应用程序数据的状态定义得不够好。

如果将异步取消与旨在支持该代码的代码一起使用,则可以这样做。通过谨慎使用pthread_setcancelstate禁用特定区域中的取消,并使用pthread_cleanup_push注册取消清除处理程序,可以使代码异步取消安全,但这不能在所有情况下都做到。

对于同步取消,如果声明为noexcept的函数未调用任何取消点函数,则一切正常。使用异步取消,所有代码都是潜在的取消点,因此在输入任何标记为noexcept的代码之前,必须调用pthread_setcancelstate暂时禁用取消,否则,如果在该函数运行时收到取消信号,则将由于取消异常而被调用。如上所述,这包括所有未明确标记为noexcept(false)

的析构函数。

因此,在使用异步取消时,对任意C库代码的任何调用(因此可能会使用析构函数构造C对象)都具有潜在的危险,并且必须调用pthread_setcancelstate来禁用创建C对象的任何代码块的取消带有析构函数,和/或无法控制地调用C库代码(例如标准库函数)。