【问题标题】:I'm trying to used std::signal to cleanly end my multithreaded program, what am I doing wrong?我正在尝试使用 std::signal 干净地结束我的多线程程序,我做错了什么?
【发布时间】:2021-07-10 05:37:36
【问题描述】:

我要做什么

我有各种必须在 linux 上同时运行的东西,直到程序被告知通过 ctrl-C 停止(在这种情况下收到 SIGINT)或服务停止(在这种情况下收到 SIGTERM)

我的想法

对于每件需要同时完成的事情,我有一个类在构造函数中启动一个线程,其析构函数使线程停止并加入它。它看起来基本上是这样的:

#include <chrono>
#include <condition_variable>
#include <mutex>
#include <thread>
#include <system_error>

class SomeClassToDoStuff
{
public:

    // Constructor
    SomeClassToDoStuff()
    {
        _thread = std::thread([this]() {
            while (true)
            {
                // Do some stuff
                ...

                // Before going on to the next iteration
                {
                    std::unique_lock<std::mutex> dataLock(_mutex);

                    // Wait for 2ms
                    if (!_shouldStopThreadExecution)
                    {
                        _conditionVariable.wait_for(dataLock, std::chrono::milliseconds(2));
                    }

                    // End the loop if requested
                    if (_shouldStopThreadExecution)
                    {
                        break;
                    }
                }
            }

            // Some cleanup
            ...
        });
    }

    // Destructor
    ~SomeClassToDoStuff()
    {
        if (_thread.joinable())
        {
            {
                std::lock_guard<std::mutex> dataLock(_mutex);
                _shouldStopThreadExecution = true;
            }

            _conditionVariable.notify_all();

            try
            {
                _thread.join();
            } catch (std::system_error const&) {}
        }
    }

private:
    mutable std::mutex _mutex;                  // Mutex to keep things thread-safe
    std::condition_variable _conditionVariable; // Condition variable used to wait
    std::thread _thread;                        // Thread in which to do the stuff
    bool _shouldStopThreadExecution = false;    // Whether to stop the thread's execution
};

那么我的main() 是这样的:

#include <atomic>
#include <chrono>
#include <csignal>
#include <iostream>
#include <thread>

namespace  {

std::atomic<int> programReturnValue(-1);  // If positive or zero, the program must return with that value

}

static void signalHandler(int sig)
{
    std::cout << "Signal received (" << sig << "). This iteration of the main loop will be the last." << std::endl;
    programReturnValue.store(0);
}

int main()
{
    // Make sure the program stops cleanly when receiving SIGTERM or SIGINT
    {
        std::signal(SIGTERM, signalHandler);
        std::signal(SIGINT, signalHandler);
    }

    SomeClassToDoStuffA a;
    SomeClassToDoStuffB b;
    SomeClassToDoStuffC c;
    SomeClassToDoStuffD d;

    while (programReturnValue.load() < 0)
    {
        // Check that everything is alright
        if (someCondition)
        {
            programReturnValue.store(1);
        }

        // Wait for 100us to avoid taking all of the processor's resources
        std::this_thread::sleep_for(std::chrono::microseconds(100));
    }

    return programReturnValue.load();
}

(顺便说一句,如果有更简单的方法来解决我有兴趣知道的这一切)

问题

当我按下 ctrl+C 或结束服务时,程序会打印出已收到信号 2 或 15(取决于我使用的哪个),然后程序结束,这很好。 然而:

  1. 清理涉及将某些内容写入文件(在执行过程中已成功写入该文件),但似乎并非总是如此,这意味着清理并未始终完全执行,即一个问题
  2. 程序的返回码不是预期的 0,甚至不是 1,而是 130 或 143,具体取决于接收到的信号

为什么会这样,我做错了什么?

编辑:据我了解,130 和 143 实际上是 128 + 信号,即如果我不尝试处理信号,程序会返回什么

Edit2:我对正在发生的事情有了更好的了解,但似乎只有一半的问题来自我的程序本身。

该程序实际上是由一个 bash 脚本运行的,然后它会打印它的返回值,并可能会根据情况重新启动它。向脚本发送 SIGINT 和 SIGTERM 也应该向程序发送 SIGTERM。

事实证明我不擅长 bash。我写过这样的东西:

#!/bin/sh

trap "killall myProgram --quiet --wait" 2 15

/path/to/myProgram&
wait $!
RETURN_VALUE=$?
echo "Code exited with return code ${RETURN_VALUE}"

# Some other stuff
...
  • 在终端中运行脚本时按 ctrl-C 实际上会导致程序同时接收 SIGINT 和 SIGTERM
  • 我打印的返回码实际上是wait+trap的结果,而不是我的程序的

我将重新编写脚本,但是两个信号都发送到我的程序这一事实是否会导致有时清理失败?如何?我该怎么办?

【问题讨论】:

  • 我目前正在处理类似的问题/设置。我喜欢带有内部条件变量的控制循环,用于控制锁定和唤醒条件。检查内部线程执行状态的析构函数也是一个好主意。可能会适应这个想法。谢谢
  • 我刚刚从en.cppreference.com/w/cpp/utility/program/signal 中读到了一些关于std::signal 的内容,显然你传递了一个非法的信号处理函数。
  • @ALX23z 怎么样?它说我可以将它传递给SIG_DFLSIG_IGN,或者一个指向签名等效于extern "C" void fun(int sig); 的函数的指针。我假设我的静态函数有正确的签名,你是说这里有一些名字修改吗?
  • @ALX23z 或者你是说我在函数中进行非法操作?与 std::atomic 交互没问题……等等,正在打印一些未定义的行为吗?
  • 请使用sigactionpthread_sigmask 明确定义信号处理行为并将处理程序执行限制为单个线程。您没有在代码或复制步骤中向我们展示某些内容 - 但sigaction 和掩码将使一切更容易理解。 (例如,如果您有两个连续的 SIGINT 并且您的系统的 signal 在处理程序调用时将处置恢复为 SIG_DFL,则您可能会遇到信号死亡。)

标签: c++ linux multithreading signals sigterm


【解决方案1】:

我对你的信号处理有点困惑:

在我看来,您似乎只使用终止系统信号来设置返回值并打破 main 中的 while 循环;线程,或者更确切地说它们的包装器被终止,即仅在它们超出范围时被破坏,这是在您的主范围的末尾,当您已经返回时!抛出的异常(在你的析构函数中)不能再被捕获。 因此,您的线程尚未结束,而您已经从 main 中返回。

作为一种解决方案:我建议在主机收到停止信号时设置停止状态_shouldStopThreadExecution。然后删除析构函数中 .join()try 语句,以便查看 quaranty 下线程的正确结束。

【讨论】:

  • 即使是main() 类也需要在返回触发器之前首先被销毁。所以我不确定你的意思。
  • 我......认为你不明白什么时候调用析构函数。如果对象是在作用域内构造的,则在离开作用域时会调用析构函数(以构造的相反顺序),无论我们是通过右大括号、返回指令还是抛出异常来调用。这也是为什么没有“抛出异常(在你的析构函数中)”的原因:析构函数必须是 noexcept,否则如果它们像这样抛出它会导致程序终止。这是一种不好的做法,而不是在我的代码中(注意 try/catch 围绕可能抛出的一件事)。
猜你喜欢
  • 2014-10-01
  • 2011-06-13
  • 2010-10-12
  • 2010-10-26
  • 2015-12-12
  • 2011-12-09
  • 2017-12-06
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多