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

时间:2014-11-07 17:03:38

标签: c++ pthreads

假设程序有多个线程:t1,t2等。这些是使用pthreads。 t2线程坐在循环读取流中并访问具有静态存储持续时间的变量。

现在假设t1调用exit(0)

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

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

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

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

3 个答案:

答案 0 :(得分:4)

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

当程序处于随机状态时使用exit - 正如您所建议的那样 - 即使使用单个线程,通常也是一种相当野蛮且不确定的方式来结束程序。如果线程在对象破坏之前或之后被破坏也无关紧要,两种方式都会导致噩梦。请记住,每个线程都可以处于随机状态并访问任何内容。并且每个线程的堆栈对象都不会被正确销毁。

请参阅exit的文档,了解它的作用和不清理: http://en.cppreference.com/w/cpp/utility/program/exit

我看到正确关闭多线程程序的优势方式,确保没有线程处于随机状态。以某种方式停止所有线程,在可行的情况下在其上调用join,并在最后剩余的线程调用exit - 或return(如果在主函数中发生这种情况)。 / p>

我经常看到的一种不正确的方法是正确处理一些对象,关闭一些句柄,并且通常尝试正确关闭,直到出现问题,然后调用terminate。我建议反对。

答案 1 :(得分:2)

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

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

这种行为往往是零星的。如果操作系统将释放的资源分配给其他进程,或者它使用了资源,那么您将看到线程崩溃。如果没有,你的线程就会运行。此行为取决于操作系统,硬件,RAM,当进程死亡时使用的资源的百分比。任何过度使用资源等等。

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

C ++是不是知道线程,所以这些线程一直运行直到C ++清理完成? C ++不知道线程。对于C ++ 11来说,这是不可能的。

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

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

答案 2 :(得分:0)

自C ++ 11起,我们有了std::thread,因此从那时起,我们可以说C ++ 了解线程。但是,这些可能不是pthreads(在Linux下,但这是实现细节),您特别提到使用过pthreads ...

我想从kris的答案中补充一件事,那就是事实是,处理线程实际上比第一个认为的要复杂一些。在大多数情况下,人们为RAII(资源获取就是初始化)创建一个类。

这里有一个带有一块内存的示例,以使其保持简单(但考虑使用std::vectorstd::array进行C ++中的缓冲区管理)

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被重新定义为不包含有关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;
};

现在,当您想使用流道时,可以重载它并实现run()函数:

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

最后,当您确保首先删除线程时,可以安全地使用此类线程。这意味着定义了第二个(由于您需要将运行程序传递给线程,因此类会强制执行!)

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,因此看不到那些无法解释的崩溃。一个很好的证明,您不想使用该特殊的分离线程功能。