3

I'm trying to create a function that can animate multiple strings to the console at the same time. By "animate," I mean print a character, wait a specified amount of time, then print the next character, and so on.

This is what I've tried so far:

/**
 @param msg        Message to animate
 @param sleep_time Time to wait between each letter
 @param wait       Whether or not to wait for the current thread to join before returning
*/
void animate(const std::string& msg, const unsigned long long sleep_time, const bool wait = true)
{
    const std::atomic<std::chrono::milliseconds> t_sleep_time =
        std::chrono::milliseconds(sleep_time);

    std::stringstream msg_strm;
    msg_strm << msg;

    std::thread animate_thread([&msg_strm, &t_sleep_time]() -> void
    {
        char letter;

        while ((letter = msg_strm.get()) != EOF)
        {
            std::cout << letter << std::flush;
            std::this_thread::sleep_for(t_sleep_time.load());
        }

        return;
    });

    if (wait)
    {
        animate_thread.join();
    }
    else
    {
        animate_thread.detach();
    }
}

This is the driver code for it:

int main()
{
    animate("Hello", 500, false);
    std::cout << '\n' << std::endl;
    animate("Welcome", 400, true);
    std::cout << "\n\nEnd" << std::endl;
}

And this is the output ("Wecome" animates as sluggishly):


Welcome

End

What happened to "Hello"? I'm very new to multi-threading, so a detailed explanation would be very much appreciated. Ideally, what I'd like to happen, is to have "Hello" animating on one line and "Welcome" on the next. Is this possible?

Drake Johnson
  • 640
  • 3
  • 19
  • 1
    A console has but one cursor. How do you plan for it to jump between the lines? – n. m. could be an AI Dec 12 '19 at 08:36
  • @n.'pronouns'm. That's fair haha. Is there a way for two threads to communicate to accomplish this? Also, is there a way to make the cursor jump back and forth, like I would need? – Drake Johnson Dec 12 '19 at 08:39
  • On another note, if you pass `false` for the `wait` argument then the `animate` function will return while the thread is still running, and that will end the life-time of the `msg_strm` and `t_sleep_time` variables, which means the references to them will become invalid. – Some programmer dude Dec 12 '19 at 08:41
  • @Someprogrammerdude Good point. I'll change that in my own code to capture by non-atomic value (since there's no writing to the variable). – Drake Johnson Dec 12 '19 at 08:43
  • There are non-portable ways to move the cursor. I don'k know of any that works seamlessly with iostreams. Research the library called "ncurses". – n. m. could be an AI Dec 12 '19 at 08:54
  • FYI: [SO: I/O in concurrent program](https://stackoverflow.com/a/48097134/7478597) – Scheff's Cat Dec 12 '19 at 08:57
  • I think you need to pass `msg_strm` by value and not by reference. – bertubezz Dec 12 '19 at 08:59
  • It's certainly possible, but the `iostream` library is really not suited to this sort of application. It does not provide the sort of control you need for this sort of task. You would be much better off using something like the ncurses library. And you do not need to have multiple threads just to do animations. You could just have an event queue and have all screen updates performed by one thread (which is much simpler to manage). – gavinb Dec 12 '19 at 12:18
  • @gavinb Does ncurses exist for Windows? – Drake Johnson Dec 12 '19 at 12:26
  • @DrakeJohnson There's [PDCurses](https://pdcurses.org) for a start, and you can always just write your own simple layer that uses ANSI escape codes to do console IO. Another option which is Win32 specific is the [Console API](https://learn.microsoft.com/en-us/windows/console/console-functions?redirectedfrom=MSDN). – gavinb Dec 13 '19 at 12:16

1 Answers1

1

First of all msg_strm lives on the stack thus you cannot pass it by value to the thread because it goes out of scope and that is why, most probable Hello was not showing. Also another problem that you have is that you are calling detach thus the program might exit before the first thread finish.

To achieve what you are trying to do I suggest to use ANSI escape codes. Therefore the following might not work on all command prompts. Also note that std::cout is not thread safe if you print in steps.

#include <atomic>
#include <iostream>
#include <string>
#include <thread>

std::atomic<int> g_lines = 1;

std::thread animate(const std::string& msg, const unsigned long long sleep_time)
{
    // NOTE: `[=]` means capture all variables used by value. Note that globals
    // are not captured.  Also note that capture by value is needed because
    // `msg` can go out-of-scope.
    return std::thread([=] {
        auto line = g_lines++;

        for (size_t column = 1; column <= msg.size(); column++)
        {
            // using `ANSI escape codes` move the cursor to the correct
            // position; \x1B[{line};{column}H

            std::cout << "\x1B[" + std::to_string(line) + ";"
                             + std::to_string(column) + "H" + msg[column - 1];

            std::this_thread::sleep_for(std::chrono::milliseconds(sleep_time));
        }
    });
}

int main()
{
    auto t1 = animate("Hello", 500);
    auto t2 = animate("Welcome", 400);

    // you need to join all threads else if you call detach, the program might
    // exit before all threads finish.
    t1.join();
    t2.join();

    std::cout << "\n\nEnd" << std::endl;
}
bertubezz
  • 361
  • 1
  • 8