【问题标题】:Invoke a method with a timeout使用超时调用方法
【发布时间】:2019-07-04 08:51:34
【问题描述】:

我想要的是调用一个方法 foo() 超时(比如 1 分钟)。如果其执行时间少于 1 分钟,则返回结果。否则会抛出异常。代码如下:

//PRINT "START" IN THE LOG
auto m = std::make_shared<std::mutex>();
auto cv = std::make_shared<std::condition_variable>();
auto ready = std::make_shared<bool>(false);
auto response = std::make_shared<TResponse>();
auto exception = std::make_shared<FooException>();
exception->Code = ErrorCode::None;

std::thread([=]
{
    std::unique_lock<std::mutex> lk(*m);
    cv->wait(lk, [=]{ return *ready; });

    try
    {
        //PRINT "PROCESS" IN THE LOG
        auto r = foo();
        *response = std::move(r);
    }
    catch(const FooException& e)
    {
        *exception = std::move(e);
    }

    lk.unlock();
    cv->notify_one();
}).detach();

std::unique_lock<std::mutex> lk(*m);
*ready = true;
cv->notify_one();
auto status = cv->wait_for(lk, std::chrono::seconds(60));
if (status == std::cv_status::timeout)
{
    //PRINT "TIMEOUT" IN THE LOG
    //throw timeout exception
}
else
{
    //PRINT "FINISH" IN THE LOG
    if (exception->Code == ErrorCode::None)
    {
        return *response;
    }
    else
    {
        throw *exception;
    }
}

您可以看到我在代码中添加了日志START/PROCESS/FINISH/TIMEOUT,每次执行此方法时,我都可以在日志中看到START/PROCESS/FINISH 或START/PROCESS/TIMEOUT 模式。但是,有时日志是 START/PROCESS,没有任何 FINISH/TIMEOUT。我认为cv-&gt;wait_for 应该最多阻塞当前线程 60 秒,然后它存在 TIMEOUT 或 FINISH。

foo() 方法包含对网络驱动器的磁盘 IO 操作,有时会挂起超过 1 小时(原因与此问题无关,现在无法解决),我尝试替换 foo使用线程睡眠,一切都按预期工作。这段代码有什么问题,我该如何改进?

【问题讨论】:

标签: c++ c++14 c++17


【解决方案1】:

因为cv-&gt;wait_for 调用中没有谓词,线程可能被虚假地解除阻塞。但是,奇怪的是没有打印 FINISH/TIMEOUT。所以我们可能需要更多信息:程序发生了什么?是否挂起,是否抛出,是否刚刚退出,是否在cv-&gt;wait_for 之后的行中打印?

您可以尝试使用std::async 并查看是否出现相同的行为(此外,它会大大简化您的代码):

std::future<int> res = std::async(foo);

std::future_status stat = res.wait_for(std::chrono::seconds(60));

if (stat != std::future_status::ready) {
  std::cout << "Timed out..." << "\n";
} else {
  try {
    int result = res.get();
    std::cout << "Result = " << result << std::endl;
  } catch (const FooException& e) {
    std::cerr << e.what() << '\n';
  }
}

编辑 正如 CuriouslyRecurringThoughts 在 cmets 中指出的那样,std::async 块在析构函数中的未来。如果这不是一个选项,下面的代码使用 std::promise 和一个分离的线程来代替:

std::promise<int> prom;
std::future<int> res = prom.get_future();

std::thread([p = std::move(prom)]() mutable {
  try {
    p.set_value(foo());
  } catch (const std::exception& e) {
    p.set_exception(std::current_exception());
  }
}).detach();

等待std::future 如前所示完成。

【讨论】:

  • 将来不是专家,所以我可能是错的,如果是这样我道歉,但是,你不应该使用 std::launch::deferred 吗?因为如果在没有策略的情况下使用 async,可以选择 std::launch::async,如果选择了,future 的析构函数会阻塞直到完成。见stackoverflow.com/questions/23455104/…
  • @CuriouslyRecurringThoughts 你完全正确,我忘记了。但是,如果通过了std::launch::deferred,则由于延迟评估,future 无法超时。编辑了我的答案。
  • 事实上我之前尝试过promise/future,它们和我当前的解决方案有同样的问题。问题是方法foo 有时会永远挂起(假设您访问网络驱动器并且服务器有错误,让您的磁盘永远等待)。如果foo 挂起,线程不能“按时”退出,我想它应该在超时超过时退出(并且分离的线程将挂在那里......)
  • 我还是不明白。当您分离创建的线程时,这意味着主线程不必与它同步,即使在 main 退出时也是如此。所以我还有一个问题:你有没有发现主线程发生了什么(它应该在超时后正常继续)?你能在超时后立即调试它或打印一些东西吗?
  • @ChengChen 你试过std::async 进行测试吗?
【解决方案2】:

似乎尽管定时等待您的主线程死锁,因为即使cv-&gt;wait_for 超时返回,它仍会尝试在当前被第二个线程锁定的互斥锁上使用lk.lock()

正如cppreference 中提到的关于wait_for

解除阻塞后,无论出于何种原因,都会重新获取锁定并退出 wait_for()。

我不确定为什么 promise/future 解决方案对您不起作用,因为您没有在此处发布该示例,但我尝试了它的简单版本,即使在第二个线程“挂起”:

using namespace std::chrono_literals;

std::cout << "START" << std::endl;
std::promise<void> p;
auto f = p.get_future();
std::thread t([p = std::move(p)]() mutable {
    std::cout << "PROCESS" << std::endl;
    std::this_thread::sleep_for(5min);
    p.set_value();
});

auto status = f.wait_for(5s);
std::cout << (status == std::future_status::ready ? "FINISH" : "TIMEOUT") << std::endl;
t.join();

输出如预期:

START
PROCESS
TIMEOUT

【讨论】:

  • 您对僵局的看法是正确的。另一种可能的解决方案是在cv-&gt;wait之后立即释放锁,即在调用foo之前,在创建的线程中(如果不需要保护foo中的数据,我猜它不需要)。您想将其添加到您的答案中吗?
  • @MikevanDyke,这正是我检查的方式,但我不认为这是一种解决方案,因为它有点违背了这个互斥锁的目的。
  • 嗯,你的意思是什么?对我来说,互斥锁的唯一目的似乎是正确使用条件变量而不是同步任何数据,所以不需要锁定它超过必要的时间?
  • @MikevanDyke,如果互斥锁用于保护条件变量,那么我们为什么要在 释放它之后调用notify_one?对我来说,这更像是一种设计模式,我们使用互斥锁来保护我们正在等待使用条件变量的数据。仅仅为了将互斥锁提供给条件变量而使用互斥锁对我来说总是有些多余和奇怪。
  • 我完全同意你的最后一句话,这就是我怀疑他的意图。因此,可以在cv-&gt;wait 之后立即释放锁,并在对foo 的调用完成后立即使用notify_one 作为信号。但我想我们将不得不等待 OP 告诉我们互斥锁的用途。
【解决方案3】:

我们可以创建一个单独的线程来运行调用本身,并在主线程中等待返回的条件变量,一旦返回,调用 foo 的线程将发出信号。

诀窍是在 60 秒超时的情况下等待条件变量,这样如果调用花费的时间超过超时时间,您仍然会醒来,了解它并能够抛出异常 - 全部在主线程中.

请在下面找到一个代码示例:

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

using namespace std::chrono_literals;

int foo()
{

    //std::this_thread::sleep_for(10s); //Will Return  Success
    std::this_thread::sleep_for(70s); //Will Return  Timeout
    return 1;
}

int foo_wrapper()
{
    std::mutex m;
    std::condition_variable cv;
    int retValue;

    std::thread t([&cv, &retValue]() 
    {
        retValue = foo();
        cv.notify_one();
    });

    t.detach();

    {
        std::unique_lock<std::mutex> lock(m);
        if(cv.wait_for(lock, 60s) == std::cv_status::timeout) 
            throw std::runtime_error("Timeout");
    }

    return retValue;    
}

int main()
{
    bool timedout = false;
    try {
        foo_wrapper();
    }
    catch(std::runtime_error& e) {
        std::cout << e.what() << std::endl;
        timedout = true;
    }

    if(!timedout)
        std::cout << "Success" << std::endl;
    else
        std::cout << "Failure" << std::endl;

    return 0;
}

如果我们在foo 中使用std::this_thread::sleep_for(10s); 将返回SUCCESS 而且,如果我们在foo 中使用std::this_thread::sleep_for(70s); 将返回TIMEOUT

希望对你有帮助!

【讨论】:

  • 我的代码有什么问题?我觉得他们很相似?
  • @ChengChen 谢谢,我编写了新代码来演示我们如何为此目的使用包装器。但是,如果您可以一起共享可编译的代码,我们可以找出代码中的问题
  • 如果你把我的代码放在你的foo_wrapper 中,你可以看到它也在工作。
  • @ChengChen 是的,我也在尝试。我认为它正在工作。那么这段代码你面临的另一个问题是什么?
【解决方案4】:

正如 Mike van Dyke 所说,documentation 非常清楚,您需要一个谓词来正确使用条件变量,以处理虚假唤醒:

当通知条件变量、超时过期或发生虚假唤醒时,线程被唤醒,互斥量被原子地重新获取。 线程应该检查条件并在唤醒是虚假的情况下继续等待。

任何使用 condvar 来等待 循环和谓词都是错误的。它应该总是有一个明确的while(!predicate)循环或者看起来像:

std::unique_lock<std::mutex> lk(*m);
auto status = cv->wait_for(lk, std::chrono::seconds(60), predicate);
if (status == std::cv_status::timeout)
{ /*...*/ } else { /*...*/ }

这意味着您需要检查一些谓词:在通知线程中的 condvar 之前设置 *ready = false(并使用 !*ready 作为谓词)就可以了。

至于为什么您没有看到预期的结果-我不知道,因为我看不到您的真实日志记录代码或您提供的代码 sn-p 之外发生的情况。从wait_for 唤醒而没有超时或收到有效响应或异常是最有可能的,但您必须调试您的代码或提供一个完整的示例来帮助解决这个问题。

【讨论】:

    猜你喜欢
    • 2013-09-01
    • 1970-01-01
    • 2011-03-28
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多