如何给用户一些分配的时间来回答?

How to give the user some assigned time to answer?

类似秒表的东西,给使用我的程序的人大约 30 秒的时间回答,如果没有回答让程序退出? 基本上响应时间不应超过给定的时间,否则程序将退出。

如果您不想使用 exit 并终止进程,您可以这样做:

std::string getInputWithin(int timeoutInSeconds, bool *noInput = nullptr)
{
    std::string answer;

    bool exceeded = false;
    bool gotInput = false;

    auto r1 = std::async([&answer, &gotInput]()
    {
        std::getline(std::cin, answer);
        gotInput = true;
    });

    auto r2 = std::async([&timeoutInSeconds, &exceeded]()
    {
        std::this_thread::sleep_for(std::chrono::seconds(timeoutInSeconds));
        exceeded = true;
    });

    while(!gotInput && !exceeded)
    {
        std::this_thread::sleep_for(std::chrono::milliseconds(1));
    }

    if(gotInput)
    {
        if(noInput != nullptr) *noInput = false;
        return answer;
    }

    if(noInput != nullptr) *noInput = true;
    return "";
}

int main()
{
    std::cout << "please answer within 10 seconds...\n";

    bool noInput;
    std::string answer = getInputWithin(10, &noInput);

    return 0;
}

这样做的好处是,您现在可以使用默认值处理丢失的输入,或者简单地给用户第二次机会,等等...

我发现 Axalo 的答案很有趣,但是 std::asyncstd::future 的不幸细节存在致命缺陷。所以我提出了一个替代方案,它避开了 std::async 但在其他方面遵循了 Axalo 的基本设计。

当我在我的平台上 运行 Axalo 的回答(符合相关细节)时,如果客户不回答,getInputWithin 永远不会 returns 或退出。该程序只是挂起。如果客户端在超时内回答正确,getInputWithin return 会给出正确答案,但在超时期限到期之前不会这样做。

这个问题的原因很微妙。 Herb Sutter's excellent paper N3630 中对其进行了详细描述。如果 return 被 std::async() 编辑,~std::future() 可以阻塞,并且会阻塞直到相关任务完成。此功能是有意放入 async/future 中的,在某些人看来,这会使 future 完全无用。

Axalo 的 r1r2 就是这样的 std::future,其析构函数应该阻塞直到相关任务完成。这就是为什么如果客户不回答,这个解决方案就会挂起。

以下是根据 threadmutexcondition_variable 构建的备选答案。它在其他方面与 Axalo 的答案非常相似,但没有(某些人认为的)std::async.

的设计缺陷
#include <chrono>
#include <condition_variable>
#include <iostream>
#include <memory>
#include <mutex>
#include <stdexcept>
#include <string>
#include <thread>
#include <tuple>

std::string
getInputWithin(std::chrono::seconds timeout)
{
    auto sp = std::make_shared<std::tuple<std::mutex, std::condition_variable,
                                          std::string, bool>>();
    std::thread([sp]() mutable
    {
        std::getline(std::cin, std::get<2>(*sp));
        std::lock_guard<std::mutex> lk(std::get<0>(*sp));
        std::get<3>(*sp) = true;
        std::get<1>(*sp).notify_one();
        sp.reset();
    }).detach();
    std::unique_lock<std::mutex> lk(std::get<0>(*sp));
    if (!std::get<1>(*sp).wait_for(lk, timeout, [&]() {return std::get<3>(*sp);}))
        throw std::runtime_error("time out");
    return std::get<2>(*sp);
}

int main()
{
    std::cout << "please answer within 10 seconds...\n";
    std::string answer = getInputWithin(std::chrono::seconds(10));
    std::cout << answer << '\n';
}

备注:

  • 时间保持在 chrono 类型系统 always 内。更喜欢类型 std::chrono::seconds 而不是具有暗示性名称的标量(int timeoutInSecondsstd::chrono::seconds timeout)。

  • 我们需要启动一个 std::thread 来处理来自 std::cin 的读取,正如 Axalo 演示的那样。然而,我们将需要 std::mutexstd::condition_variable 来进行通信,而不是使用 std::future 的便利性。主线程和这个辅助线程都需要共享这些通信对象的所有权,我们不知道哪个先死。如果客户端从不响应,辅助线程可能永远存在,造成有效的内存泄漏,这是这里没有解决的另一个问题。但无论如何,共享所有权的最简单方法是使用 copied std::shared_ptr 存储通信对象。最后一个熄灯了

  • 启动一个 std::thread 等待 std::cin 并在主线程得到它时向主线程发出信号。信号必须在 mutex 锁定的情况下完成。请注意,此 thread 可以(实际上必须)分离。 thread 不能触及任何不属于它的内存(因为 shared_ptr 拥有所有引用的内存)。如果 main 在辅助线程处于 运行ning 时退出,OS 将在没有 UB 的情况下优雅地关闭线程。

  • 主线程然后锁定 mutex 并使用指定的超时在 condition_variable 上执行 wait_for,以及检查 bool中的tuple转为true。此 wait_for 将 return 提早 bool 设置为 true,或者将 return 设置为 false 后 [=55] =]秒。如果他们比赛(超时和客户同时回答)没关系,要么会有 string 那里或没有,booltuple 回答那个问题。尽管 主线程正在执行 wait_formutex 已解锁,因此辅助线程可以使用它。

  • 如果主线程returns和tuple中的bool没有设置为true,则抛出异常.如果这个异常没有被捕获,std::terminate() 将被调用。否则,tuple中的string会有客户端的响应。

  • 这种方法很容易受到客户端创建许多它从不回答的响应的影响,从而有效地增加 shared_ptr 永远不会被破坏的内存泄漏。解决这个问题不是我知道如何在可移植 C++ 中做的事情。

在 C++14 中,可以对 getInputWithin 进行轻微修改,从而减少选择错误的 tuple 成员的错误。由于我们的 tuple 由所有不同的类型组成,我们可以按类型而不是按位置对其进行索引:

std::string
getInputWithin(std::chrono::seconds timeout)
{
    auto sp = std::make_shared<std::tuple<std::mutex, std::condition_variable,
                                          std::string, bool>>();
    std::thread([sp]() mutable
    {
        std::getline(std::cin, std::get<std::string>(*sp));  // here
        std::lock_guard<std::mutex> lk(std::get<std::mutex>(*sp));  // here
        std::get<bool>(*sp) = true;  // here
        std::get<std::condition_variable>(*sp).notify_one();  // here
        sp.reset();
    }).detach();
    std::unique_lock<std::mutex> lk(std::get<std::mutex>(*sp));  // here
    if (!std::get<std::condition_variable>(*sp).wait_for(lk, timeout,
                                           [&]() {return std::get<bool>(*sp);}))  // here
        throw std::runtime_error("time out");
    return std::get<std::string>(*sp);  // here
}

也就是说,标记为 // here 的行已更改为 std::get<type>(*sp) 而不是 std::get<index>(*sp)

更新

受到下面 TemplateRex 的好评的启发,我有点偏执,我添加了对 sp.reset() 的调用作为辅助线程所做的最后一件事。这迫使主线程成为破坏 tuple 的线程,消除了辅助线程在破坏其 sp 的本地副本之前停止的可能性,并让主线程通过 atexit 链,然后让辅助线程唤醒并 运行 tuple 析构函数。

可能还有其他原因使对 sp.reset() 的调用变得不必要。但是有了这种预防药,我们就不用担心了。