Что произойдет, если вы вызовете exit (0), пока другие потоки все еще запущены?

Предположим, что программа имеет несколько потоков: t1, t2 и т.д. Они используют pthreads. Поток t2 сидит в цикле, читая из потока и обращаясь к переменной со статической продолжительностью хранения.

Теперь предположим, что t1 вызывает exit(0).

(Дополнительные сведения: у меня есть программа, которая делает это в системе на основе Unix, и скомпилирована с g++. Программа, по-видимому, иногда дает сбой при завершении работы с трассировкой стека, которая указывает на недопустимую статическую переменную.)

  • Поток уничтожается до уничтожения объекта C++?

  • C++ не знает о потоках, поэтому они продолжают работать до тех пор, пока очистка C++ не будет завершена?

  • Должен ли обработчик SIGTERM сначала отключать или уничтожать потоки, прежде чем продолжить, или это происходит автоматически?

Ответ 1

Я отвечаю на вопрос в названии вашего вопроса, а не на три пункта с маркером, потому что я думаю, что ответы на вопросы с маркером не имеют значения для ответа на реальный вопрос.

Использование exit, когда программа находится в случайном состоянии - как вы, похоже, предлагаете - обычно является довольно жестоким и недетерминированным способом завершить программу даже одним потоком. Даже не имеет значения, будет ли поток уничтожен до уничтожения объекта или после него, оба пути приводят к кошмарам. Помните, что каждый поток может находиться в произвольном состоянии и иметь доступ ко всему. И объекты стека каждого потока не будут уничтожены должным образом.

См. документацию exit, чтобы узнать, что она делает, а что нет.

Самый лучший способ, которым я видел правильное завершение многопоточной программы, - убедиться, что ни один поток не находится в случайном состоянии. Остановите все потоки тем или иным способом, вызовите join для них, где это возможно, и из последнего оставшегося потока вызовите exit - или return, если это происходит в основной функции.

Неправильный подход, который я часто видел, состоит в том, чтобы правильно избавиться от некоторых объектов, закрыть некоторые дескрипторы и вообще попытаться сделать правильное завершение работы, пока все не пойдет не так, и затем вызвать terminate. Я советую против этого.

Ответ 2

Позвольте мне попытаться ответить на ваши вопросы. Ребята, поправьте меня, если я ошибаюсь.

Ваша программа периодически падает. Это ожидаемое поведение. Вы освободили все приобретенные ресурсы. И ваш поток, который жив, пытается получить доступ к ресурсам, основываясь на информации, которую он имеет. Если он будет успешным, он будет запущен. Если это не удастся, это сработает.

Часто поведение было бы спорадическим. Если ОС выделяет выделенные ресурсы другим процессам или использует ресурсы, вы увидите, что ваш поток рушится. Если нет, ваш поток работает. Такое поведение зависит от ОС, аппаратного обеспечения, ОЗУ,% от ресурсов, используемых при завершении процесса. Любое использование ресурсов и т.д. И т.д.

Удаляется ли поток до уничтожения объекта С++? Нет. В С++ нет встроенной поддержки потоков. P-потоки - это только потоки posix, которые работают с базовой ОС и предоставляют вам функциональность для создания потоков, если это необходимо. С технической точки зрения, поскольку потоки не являются частью С++, потоки, автоматически убиваемые, невозможны. Исправьте меня, если я ошибаюсь.

Не известно ли С++ о потоках, поэтому они продолжают работать до тех пор, пока очистка С++ не будет завершена? С++ не знает о потоках. То же самое нельзя сказать о С++ 11

Должен ли обработчик SIGTERM сначала отключать или убивать потоки, прежде чем продолжить, или это произойдет автоматически? Технически обработчик SIGTERM не должен убивать потоки. Почему вы хотите, чтобы обработчики ОС убивали текущие потоки? Каждая операционная система работает на аппаратном обеспечении для обеспечения функциональности для пользователей. Не убивать ни один из запущенных процессов. Ну, программисты должны присоединиться к потокам в основном, но могут быть случаи, когда вы хотите, чтобы ваши потоки запускались некоторое время. Может быть.

Ответственность разработчика программного обеспечения/поставщика заключается в написании кода, который не разбивается и не заканчивается в бесконечных циклах, и при необходимости убивать все выполняемые потоки. ОС не может взять на себя ответственность за эти действия. Именно по этой причине Windows/Apple сертифицирует некоторые программные средства для своих ОС. Таким образом, клиенты могут купить это с душевным спокойствием.

Ответ 3

Начиная с C++ 11 у нас есть std::thread, и с тех пор мы можем сказать, что C++ знает о потоках. Однако это могут быть не pthreads (это под Linux, но это деталь реализации), и вы специально упомянули, что использовали pthreads...

Одна вещь, которую я хотел добавить из ответа Криса, это то, что на самом деле работать с потоками немного сложнее, чем кажется на первый взгляд. В большинстве случаев люди создают один класс для RAII (Resource Acquisition Is Initialization.)

Вот пример с блоком памяти, чтобы сохранить его простым (но рассмотрите возможность использования std::vector или std::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().

Бегунок переопределен, чтобы не включать ничего о 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...
    }
};

Наконец, вы можете использовать это безопасно, когда убедитесь, что поток будет удален первым. Это означает определенную секунду (которую применяют классы, так как вам нужно передать бегун в поток!)

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;
}

Я уверен, что многие люди прокомментируют тот факт, что вы никогда не должны использовать исключение для выхода из вашего программного обеспечения. Дело в том, что в большинстве моих программ есть такая попытка/отлов, поэтому я могу, по крайней мере, зарегистрировать ошибку, которая произошла, и она очень похожа на использование "исключения при выходе".

ВНИМАНИЕ: std::thread отличается от моего класса thread выше. Он принимает указатель на функцию для выполнения некоторого кода. Он вызовет pthread_join() (по крайней мере, в g++ Linux) при уничтожении. Тем не менее, он ничего не говорит вашему коду потока. Если вам нужно прослушать какой-то сигнал, чтобы узнать, что он должен выйти, вы отвечаете. Это совершенно другой способ мышления, однако его также можно безопасно использовать (за исключением этого пропущенного сигнала).

Для полной реализации вы можете посмотреть мой файл snap_thread.cpp/h на Github в Snap! C++ проект. Моя реализация включает в себя гораздо больше функций, в частности, FIFO, который вы можете использовать для безопасной передачи рабочих нагрузок вашим потокам.

Как насчет отсоединения темы?

Я тоже использовал это некоторое время. Дело в том, что только pthread_join() безопасен на 100%. Отключение означает, что поток все еще работает, и выход из основного процесса может привести к сбою потока. Хотя, в конце концов, я бы сказал потоку выйти и дождался установки "готового" сигнала, но он все равно время от времени падал. Может пройти около 3 месяцев или около того, прежде чем я увижу необъяснимую аварию, но это случится. Поскольку я удалил это и всегда использую объединение, я не вижу этих необъяснимых сбоев. Хорошее доказательство того, что вы не хотите использовать эту особую функцию отсоединения потока.