如果在其他线程仍在运行时调用exit(0),会发生什么情况

What happens if you call exit(0) while other threads are still running?

本文关键字:什么情况 exit 线程 其他 运行时 如果 调用      更新时间:2023-10-16

假设一个程序有几个线程:t1、t2等。这些线程使用pthreads。t2线程处于从流中读取并访问具有静态存储持续时间的变量的循环中。

现在假设t1调用exit(0)

(更多详细信息:我有一个程序在基于Unix的系统上执行此操作,并使用g++编译。该程序在关闭时偶尔会崩溃,堆栈跟踪表明静态变量无效。)

  • 线程是否在C++对象销毁之前被杀死?

  • C++是否不知道线程,所以这些线程一直运行到C++清理完成?

  • SIGTERM处理程序应该在继续之前先关闭或终止线程,还是自动发生?

我在回答你问题标题中的问题,而不是3个要点,因为我认为要点问题的答案与实际问题的答案无关。

当程序处于随机状态时使用exit——正如你所建议的那样——通常是一种相当残酷和不确定的方式来结束程序,即使只有一个线程。线程是在对象破坏之前还是之后被破坏都无关紧要,这两种方式都会导致噩梦。请记住,每个线程都可能处于随机状态并访问任何内容。并且每个线程的堆栈对象将不会被正确地销毁。

请参阅exit的文档,了解它可以清理哪些内容和不清理哪些内容。

我看到正确关闭多线程程序的最常用方法是确保没有线程处于随机状态。以某种方式停止所有线程,在可行的情况下对其调用join,如果在主函数中发生这种情况,则从最后一个剩余线程调用exit-或return

我经常看到的一种不正确的方法是正确地处理一些对象,关闭一些句柄,通常尝试正确地关闭,直到一切都出错,然后调用terminate。我建议不要那样做。

让我试着回答你的问题。伙计们,如果我做错了,请纠正我。

您的程序偶尔会崩溃这是预期的行为。您已经释放了所有获得的资源。而你的线程,它是活的,正试图访问资源,基于它所拥有的信息。如果成功,它将运行。如果不成功,它就会崩溃。

这种行为通常是偶发的。如果操作系统将释放的资源分配给其他进程,或者使用这些资源,那么您将看到线程崩溃。如果没有,线程将运行。此行为取决于操作系统、硬件、RAM以及进程终止时使用的资源百分比。过度使用资源等。

线程是否在C++对象销毁之前被杀死没有。C++没有任何对线程的内置支持。P线程只是posix线程,它与底层操作系统一起工作,并在需要时为您提供创建线程的功能。从技术上讲,由于线程不是C++的一部分,线程不可能自动终止。如果我错了,请纠正我。

C++是否不知道线程,所以这些线程会一直运行直到C++清理完成C++不知道线程。对于C++11 来说,情况并非如此

SIGTERM处理程序应该在继续之前先关闭或终止线程,还是自动发生从技术上讲,SIGTERM处理程序不应该终止线程。为什么您希望操作系统处理程序杀死正在运行的线程?每个操作系统都在硬件上工作,为用户提供功能。不要杀死任何正在运行的进程。程序员必须将线程连接到main,但在某些情况下,您可能希望让线程运行一段时间。可能是。

软件开发人员/供应商有责任编写不会崩溃或以无限循环结束的代码,并在需要时杀死所有正在运行的线程。OS不能对这些行为承担责任。这就是为什么Windows/Apple为其操作系统认证一些软件的原因。因此,客户可以放心购买。

由于C++11,我们有std::thread,因此从那时起,我们可以说C++知道线程。然而,这些可能不是pthread(它在Linux下,但它是一个实现细节),您特别提到您使用了pthread。。。

我想从kris的回答中补充的一点是,处理线程实际上比最初想象的要复杂一点。在大多数情况下,人们为RAII(资源获取即初始化)创建一个类

这里有一个内存块的例子,为了简单起见(但考虑在C++中使用std::vectorstd::array进行缓冲区管理):

class buffer
{
public:
    buffer()
        : m_buffer(new char[1024])
    {
    }
    ~buffer()
    {
        delete [] m_buffer;
    }
    char * data()
    {
        return m_buffer;
    }
private:
    char * m_buffer;
};

这个类所做的是管理m_buffer指针的生存期。在构建时,它会分配一个缓冲区,在销毁时,它就会释放它。到目前为止,还没有什么新鲜事。

然而,对于线程,您会遇到一个潜在的问题,因为线程在中运行的类在被破坏之前必须保持良好的状态,而且正如许多C++程序员所知,一旦到达析构函数,就太晚了,无法执行某些操作。。。特别是调用任何虚拟函数。

所以下面的一个基本类实际上是不正确的:

// you could also hide this function inside the class, see "class thread" below
class runner;
void start_func(void * data)
{
    ((runner *) data)->run();
}
class runner
{
public:
    runner()
    {
        // ...setup attr...
        m_thread = pthread_create(&m_thread, &attr, &start_func, this);
    }
    virtual ~runner()
    {
        stop();   // <-- virtual function, we may be calling the wrong one!
        pthread_join(m_thread);
    }
    virtual void run() = 0;
    virtual void stop()
    {
        m_stop = true;
    }
private:
    pthread_t m_thread;
    bool m_stop = false;
};

这是错误的,因为stop()函数可能需要调用类的派生版本中定义的某个虚拟函数。此外,您的run()函数在存在之前很可能会使用虚拟函数。其中一些可能是纯虚拟函数。调用被调用的~runner()函数中的那些函数将以std::terminate()结束。

这个问题的解决办法是有两个类。一个带有run()纯虚拟函数的运行程序和一个线程。线程类负责删除pthread_join()之后的runner。

runner被重新定义为不包含任何关于pthread:的内容

class runner
{
public:
    virtual void run() = 0;
    virtual void stop()
    {
        m_stop = true;
    }
private:
    bool m_stop = false;
};

线程类处理stop(),这可能发生在其析构函数中

class thread
{
public:
    thread(runner *r)
        : m_runner(r)
    {
        // ...setup attr...
        m_thread = pthread_create(&m_thread, &attr, &start_func, this);
    }
    ~thread()
    {
        stop();
    }
    void stop()
    {
        // TODO: make sure that a second call works...
        m_runner->stop();
        pthread_join(m_thread);
    }
private:
    static void start_func(void * data)
    {
        ((thread *) data)->start();
    }
    void start()
    {
        m_runner->run();
    }
    runner * m_runner;
    pthread_t m_thread;
};

现在,当你想使用你的runner时,你可以重载它并实现一个run()函数:

class worker
    : runner
{
public:
    virtual void run()
    {
        ...do heavy work here...
    }
};

最后,当您确保线程首先被删除时,您可以安全地使用它。这意味着定义的秒(类强制执行,因为您需要将一个runner传递给线程!)

int main()
{
    worker w;
    thread t(&w);
    ...do other things...
    return 0;
}

现在,在本例中,C++负责清理,但这只是因为我使用了return,而不是exit()

然而,你的问题的解决方案是例外的。我的main()也是例外安全!在调用std::terminate()之前,线程将完全停止(因为我没有try/catch,所以它将终止)。

实现"从任何地方退出"的一种方法是创建一个允许您这样做的异常。所以main()会变成这样:

int main()
{
    try
    {
        worker w;
        thread t(&w);
        ...do other things...
    }
    catch(my_exception const & e)
    {
        exit(e.exit_code());
    }
    return 0;
}

我相信很多人都会评论这样一个事实:你永远不应该使用异常来退出你的软件。事实上,我的大多数软件都有这样的try/catch,所以我至少可以记录发生的错误,这与使用"退出异常"非常相似。

警告:std::thread与我上面的thread类不同。它接受一个函数指针来执行一些代码。它将在销毁时调用pthread_join()(至少在g++Linux上)。但是,它不会告诉线程代码任何信息。如果你需要听一些信号来知道它必须退出,你就是负责人。这是一种完全不同的思维方式,然而,它也可以安全使用(除了丢失的信号之外)。

要获得完整的实现,您可能需要在snap中查看我在Github上的snap_thread.cpp/h!C++项目。我的实现包括更多的功能,尤其是它有一个FIFO,您可以使用它将工作负载安全地传递给线程。

如何分离线程

我也用了一段时间。事实是,只有pthread_join()是100%安全的。分离意味着线程仍在运行,主进程退出可能会使线程崩溃。尽管最后我会告诉线程退出,并等待设置"完成"信号,但它仍然会偶尔崩溃。我可能需要3个月左右的时间才能看到一场无法解释的车祸,但它确实会发生。由于我删除了它并一直使用join,所以我没有看到那些无法解释的崩溃。很好地证明了您不想使用特殊的分离线程功能。

相关文章: