3

I have an std::thread that might be blocked on a file descriptor input/output call, how can I cleanly cancel it?

Consider the following sample:

#include <unistd.h>
#include <thread>

void thread_routine(int fd)
{
    char buf;
    read(fd, &buf, 1);
}

int main()
{
    int pipefd[2];
    pipe(pipefd);

    std::thread thread(&thread_routine, pipefd[0]);

    thread.join();

    close(pipefd[0]);
    close(pipefd[1]);
}

What can I do before the join() to be sure that it will not lock forever? (The pipe is just a quick sample way to get a file descriptor, I have a more exotic scenario but I'm trying to get a generic answer.)

With pthreads I could call pthread_cancel() because read() and write() are cancellation points, but there's no C++ way to cancel an std::thread (I could get the thread::native_handle() and pass it to pthread_cancel(), but I'd like a cleaner approach).

Please note that:

  • I can't set the file descriptor in non blocking mode
  • I can't use select() on the file descriptor
Lightness Races in Orbit
  • 378,754
  • 76
  • 643
  • 1,055
lornova
  • 6,667
  • 9
  • 47
  • 74

2 Answers2

2

Using std::future, you can block in limited intervals and eventually decide to stop waiting and detach the thread. Leaking a running thread won't matter if your going to exit anyway. If not exiting, then either the blocking call will return naturally and the thread will be destroyed, or it will be blocked for the rest of the process' lifetime anyway.

In the following example MAX_WAIT_TIME is used as a condition for detaching and exiting.

#include <chrono>
#include <future>
#include <thread>

using namespace std::literals;

const auto BLOCK_DURATION = 100ms;
const auto MAX_WAIT_TIME = 3s;

int main() {
    // Some blocking call.
    auto blocking_call = [] { std::this_thread::sleep_for(1min); };

    // Wrap the callable in a packaged task and get future.
    std::packaged_task<void()> task{blocking_call};
    auto future = task.get_future();

    auto start_time = std::chrono::steady_clock::now();
    std::thread t{std::move(task)};

    // Continually check if task is finished, i.e., if blocking call has returned.
    auto status = future.wait_for(0s);
    while (status != std::future_status::ready) {
        status = future.wait_for(BLOCK_DURATION);

        // Room for condition to stop blocking, e.g., check some signaling etc.
        auto duration = std::chrono::duration_cast<std::chrono::seconds>(std::chrono::steady_clock::now() - start_time);
        if (duration > MAX_WAIT_TIME) {
            break;
        }
    }

    if (status == std::future_status::ready) {
        future.get(); // Optionally get return value.
        t.join();
    } else {
        t.detach();
    }

    /* And we're on our way... */
}

std::future::wait_for will immediately return when the call returns, even if the full wait duration have not yet come to pass.

If time is the limit for how long the call may block, then there's even the neat std::future::wait_until. E.g.:

auto start_time = std::chrono::steady_clock::now();
std::thread t{std::move(task)};

auto status = future.wait_until(start_time + MAX_WAIT_TIME);

if (status == std::future_status::ready) {
    t.join();
} else {
    t.detach();
}
Felix Glas
  • 15,065
  • 7
  • 53
  • 82
1

I believe the answer is system dependent. There are some ways to do it and it might work differently in different OS'es.

  1. Close the file descriptor. Than operations will not block but will always end up with errors. Also blocked operations will end up with an error.

  2. Another way will be to send a signal which will be handled by this thread and interrupt any system operations.

  3. (if you can use select) Prepend a read() call with a select() and try approach 1.

  4. (if you can use select) Add another signalling socket and select on both. Than exit if signalled socket can be read.

  5. (if you can use select) Probably it makes sense to stick all the logic into one thread. For example NTPD which works with GPS/Sockets/Stdio is a single threaded application with just one select!

I think approach 4 is the best. But if you can't use select you can try 2 and 1

Don't use pthread_cancel. It's really bad practice.

norekhov
  • 3,915
  • 25
  • 45
  • With select() it is undefined behaviour: http://stackoverflow.com/questions/543541/what-does-select2-do-if-you-close2-a-file-descriptor-in-a-separate-thread It is fine to do it with read()? – lornova Apr 23 '17 at 22:24
  • In reply to your update: I can't use select() - the file descriptor I'm using doesn't supports it. – lornova Apr 23 '17 at 22:26
  • Well it doesn't say it's "undefined". It just says different OS'es behave differently. We used this practice in QNX and it was perfectly ok. I believe for Linux it's also ok. Also please note that even Linux is not always POSIX. – norekhov Apr 23 '17 at 22:29
  • Probably for you sending a signal (and handling in properly of course) will be a good approach. – norekhov Apr 23 '17 at 22:40
  • I'm not sure what's actually applicable on my specific scenario but I'll try them. However I definitely can't stick everything in one thread. (What's NTPD?) – lornova Apr 23 '17 at 22:51
  • 1
    NTPD is a well-known open-source application for system time synchronization which handles data from different sources. You can check the code in their repo if you're interested. It's one thread with one select which handles all IO operations including network, serial ports, stdio e.t.c. – norekhov Apr 23 '17 at 22:53