Вызвать метод с тайм-аутом - PullRequest
6 голосов
/ 04 июля 2019

Я хочу вызвать метод 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 в журналах. Тем не менее, иногда журналы запускаются / обрабатываются без каких-либо завершений / тайм-аутов. Я думаю, cv->wait_for должен блокировать текущий поток максимум на 60 секунд, тогда он существует либо с TIMEOUT, либо с FINISH.

Метод foo() содержит операции ввода-вывода диска с сетевыми дисками, которые иногда зависают более 1 часа (причина не связана с этим вопросом и не может быть решена сейчас), я попытался заменить foo с нитью спит, все работает как положено. Что не так с этим кодом и как я могу улучшить это?

Ответы [ 4 ]

4 голосов
/ 11 июля 2019

Кажется, что, несмотря на временное ожидание, ваш главный поток заходит в тупик, потому что даже когда cv->wait_for возвращается с таймаутом, он все еще пытается lk.lock() на мьютексе, который в данный момент заблокирован вторым потоком.

Как уже упоминалосьon cppreference about wait_for:

Если разблокировано, независимо от причины , блокировка возобновляется, и wait_for () завершается.

Я не уверен, почему решение «обещание / будущее» не сработало для вас, поскольку вы не опубликовали этот пример здесь, но я попробовал простую версию, которая, кажется, работает, даже когда второй поток"зависает":

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
4 голосов
/ 09 июля 2019

Поскольку у вас нет предиката в вызове cv->wait_for, поток может быть случайно заблокирован. Однако странно, что FINISH / TIMEOUT не печатается. Поэтому нам может потребоваться дополнительная информация: что происходит с программой? Он зависает, выдает, просто выходит, печатает ли в строке после cv->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 о будущем 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 выполняется, как показано ранее.

1 голос
/ 04 июля 2019

Мы можем создать отдельный поток для запуска самого вызова и ожидать переменную условия в вашем основном потоке, которая будет сообщена потоком, выполняющим вызов 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;
}

Если мы используем std::this_thread::sleep_for(10s); внутри foo, вернемся SUCCESS И, если мы используем std::this_thread::sleep_for(70s); внутри foo, вернемся TIMEOUT

Надеюсь, это поможет!

0 голосов
/ 11 июля 2019

Как говорит Майк ван Дайк, и документация совершенно ясно указывает на необходимость предиката для правильного использования условной переменной, чтобы справиться с ложными пробуждениями:

Когда переменная условия уведомляется, истекает тайм-аут или происходит ложное пробуждение, поток пробуждается, и мьютекс атомарно восстанавливается. Затем поток должен проверить состояние и возобновить ожидание, если пробуждение было ложным.

Любое использование 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 { /*...*/ }

, что означает, что вам нужен некоторый предикат для проверки: установка *ready = false перед уведомлением condvar в вашем потоке (и использование !*ready в качестве предиката) будет в порядке.

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

...