Skip to main content

Raymii.org Logo (IEC resistor symbol) logo

Quis custodiet ipsos custodes?
Home | About | All pages | RSS Feed | Gopher

C++ async, threads and user input

Published: 24-04-2020 | Author: Remy van Elst | Text only version of this article


Table of Contents


For an unrelated piece of code, I recently spent a few days trying to figure out if there was a portable, modern C++ way to handle user input with a timeout. If there is no input after a few seconds the program can continue doing other things. TL;DR, there is none, since stdin is blocking I/O.

alarm, conio.h, using ncurses or manually polling stdin are all way to complex for the scope of the program. I ended up with using two std::threads, one for input and one for the "other things". I did play with std::future and std::async since that way is easier to just 'wait until this is done' as opposed to manually managing 'actual big boy' threads.

This article has example code that uses std::async in an attempt to wait until the user has given some input and otherwise quit after 5 seconds. It does not work since std::getline is blocking. The main() function ends, but the async function is still waiting for user input.

If you like this article, consider sponsoring me by trying out a Digital Ocean VPS. With this link you'll get $100 credit for 60 days). (referral link)

At the end of the article I'll also provide the code I ended up using, with the two threads, one for input and one for 'other work'.

The async code

Below is a single test.cpp file. It is not my actual program, but a simplified version to show

#include <iostream>
#include <string>
#include <future>
#include <chrono>

std::string getString()
{
  std::cout << "# getString() start\n";
  std::cout << "# getString(): Please enter a string. You have 5 seconds...\n";
  std::string input;
  std::getline(std::cin, input);
  std::cout << "# getString() end\n";
  return input;
}

int main()
{
  std::cout << "# main() start\n";
  std::cout << "# main(): Starting std::async(getString)\n";

  std::future<std::string> futureString = std::async(std::launch::async, getString);

  std::cout << "# main(): Waiting 5 seconds for input...\n";

  std::chrono::system_clock::time_point five_seconds_passed
          = std::chrono::system_clock::now() + std::chrono::seconds(5);
  std::future_status status = futureString.wait_until(five_seconds_passed);

  if (status == std::future_status::ready)
  {
      auto  result = futureString.get();
      std::cout << "# main(): Input result: " << result << "\n";
  }
  else
  {
      std::cout << "# main(): Timeout reached... \n";
  }

  std::cout << "# main() end" << std::endl;
  return 0;
}

Make sure you pass -pthread while compiling. In CMake:

find_package(Threads REQUIRED)
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -pthread" )

The result

The below gif shows the program when input is given within five seconds:

input

Textual output:

# main() start
# main(): Starting std::async(getString)
# main(): Waiting 5 seconds for input...
# getString() start
# getString(): Please enter a string. You have 5 seconds...
    ===== RETURN PRESSED ON KEYBOARD =====
# getString() end
# main(): Input result: 
# main() end

Process finished with exit code 0

The below gif shows the program when input is not given on time (within 5 sec):

no input

Textual output:

# main() start
# main(): Starting std::async(getString)
# getString() start
# getString(): Please enter a string. You have 5 seconds...
# main(): Waiting 5 seconds for input...
# main(): Timeout reached... 
# main() end
   ===== RETURN PRESSED ON KEYBOARD =====
# getString() end

Process finished with exit code 0

As you can see, the async thread will stay running until the user has given some input, then the program ends. The timeout we want is sort of available, the main function does continue. But, stopping the user input thread does not happen. This, again, is because the getline() call is blocking. The thread will stop after the call is complete. I did try other tricks such as putting the terminal in nonblocking mode or manually polling with poll(), but those all were not portable (windows/linux) or involved memcpy and more C like code than I like, as opposed to modern C++.

Conclusion and alternative solution

I didn't get to my goal of having user input with a timeout. For the program, it turns out that a solution with two threads, one for input and one for 'other work' was a better choice. There is no timeout on the user input, whenever input is received it is handled and signaled to the main thread. Here below is a simplified version with a thread that is 'doing work' and one that handles input. If there is specific input, it does a thing and quits.

Here is a GIF that shows the program:

thread

Textual output:

Please enter a command: 
# (3 seconds pass)
I'm doing other work... 
# (3 seconds pass)
I'm doing other work...
# user input is given:
magic
The answer to life, the universe and everything!

Below is the file, single file again as above:

#include <iostream>
#include <memory>
#include <string>
#include <thread>
#include <mutex>
#include <chrono>
#include <atomic>

class Example {
    std::atomic<bool> running;
    std::atomic<bool> renderedText;
    std::mutex inputMutex;
    std::mutex otherThingsMutex;
    std::thread otherThread;
    std::thread inputThread;

    void renderText() {
        if(!renderedText) {
            renderedText = true;
            std::cout << "Please enter a command: " << std::endl;
        }
    }

    static void doSomethingWithInput(const std::string& input) {
        if (input == "magic")
            std::cout << "The answer to life, the universe and everything!" << std::endl;
    }

public:
    Example() : running(true), renderedText(false),
                otherThread(&Example::otherThings, this),
                inputThread(&Example::input, this)
    {
    }

    ~Example() {
        inputThread.join();
        otherThread.join();
    }

    inline void quit() {
        running = false;
    }

    void handleInput() {
        std::string input;
        std::getline(std::cin, input);
        doSomethingWithInput(input);
        quit();
    }

    static void doOtherTask() {
        std::cout << "I'm doing other work..." << std::endl;
        std::this_thread::sleep_for(std::chrono::seconds(3));
    }

    void input() {
        while (running) {
            std::lock_guard<std::mutex> locker(inputMutex);
            handleInput();
        }
    }

    void otherThings() {
        while (running) {
            std::lock_guard<std::mutex> locker(otherThingsMutex);
            renderText();
            doOtherTask();
        }
    }
};

int main() {
    std::unique_ptr<Example> test = std::make_unique<Example>();
    return 0;
}
Tags: articles , async , await , c++ , cmake , cpp , development , future , stdin , threads