I am trying to implement a feature to print multiple progress bars on the screen using threads. I have a ProgressBar class which produces progress bars (in case you need more info about it you can look here, the mutex locks have been removed from that), it has an update method, to update the bar inside a loop.
Then, I am creating a new MultiProgressBar class to manage multiple progress bars with threads and display them at the same time in the terminal.
#ifndef MULTIPROGRESSBAR_H
#define MULTIPROGRESSBAR_h
#include <iostream>
#include <type_traits>
#include <tuple>
#include <functional>
#include <mutex>
#include <atomic>
#include <utility>
namespace osm
{
template <size_t... Is>
struct indices {};
template <size_t N, size_t... Is>
struct gen_indices: gen_indices <N - 1, N - 1, Is...> {};
template <size_t... Is>
struct gen_indices <0, Is...>: indices<Is...> {};
template <class... Indicators>
class make_MultiProgressBar
{
public:
template <class... Inds>
make_MultiProgressBar( Inds&&... bars ): bars_{ std::forward <Inds> ( bars )... } {}
static size_t size() { return sizeof...( Indicators ); }
template <class Func, class... Args>
void for_one( size_t idx, Func&& func, Args&&... args )
{
call_one( idx, gen_indices <sizeof...( Indicators )> (), std::forward <Func> ( func ), std::forward <Args> ( args )... );
}
template <class Func, class... Args>
void for_each( Func&& func, Args&&... args )
{
call_all( gen_indices <sizeof...( Indicators )> (), std::forward <Func> ( func ), std::forward <Args> ( args )... );
}
private:
template <size_t... Ids, class Func, class... Args>
void call_one( size_t idx, indices <Ids...>, Func func, Args&&... args )
{
std::lock_guard<std::mutex> lock{mutex_};
[](...) {}
(
(idx == Ids &&
( ( void ) std::forward <Func> ( func )( std::get <Ids> ( bars_ ),
std::forward <Args> ( args )... ), false ) )...
);
}
template <size_t... Ids, class Func, class... Args>
void call_all( indices <Ids...>, Func func, Args&&... args )
{
auto dummy = { ( func( std::get <Ids>( bars_ ), args...), 0 )... };
( void )dummy;
}
std::tuple <Indicators&...> bars_;
std::mutex mutex_;
};
template <class... Indicators>
make_MultiProgressBar <typename std::remove_reference <Indicators>::type...>
MultiProgressBar( Indicators&&... inds )
{
return { std::forward <Indicators> ( inds )... };
}
template <class T>
struct type_identity
{
using type = T;
};
struct updater
{
template <template <class> class PB, class bar_type>
auto operator()( PB <bar_type>& pb, typename type_identity <bar_type>::type v ) const
-> decltype( pb.update( bar_type{} ) )
{
return pb.update( v );
}
};
}
#endif
and in a main program should work as:
#include <iostream>
#include <thread>
#include <chrono>
#include <cmath>
#include <iomanip>
#include "../include/osmanip.h" //Header containing ProgressBar class and others.
using namespace osm;
using namespace std;
using namespace std::this_thread;
using namespace std::chrono;
ProgressBar<int> prog_int;
prog_int.setMin( 0 );
prog_int.setMax ( 100 );
prog_int.setStyle( "complete", "%", "#" );
prog_int.setBrackets( "[", "]" );
ProgressBar<int> prog_int_2;
prog_int_2.setMin( 5 );
prog_int_2.setMax ( 25 );
prog_int_2.setStyle( "complete", "%", "#" );
prog_int_2.setBrackets( "[", "]" );
ProgressBar<float> prog_float;
prog_float.setMin( 0.1f );
prog_float.setMax ( 12.1f );
prog_float.setStyle( "complete", "%", "#" );
prog_float.setBrackets( "[", "]" );
auto bars = MultiProgressBar( prog_int, prog_int_2, prog_float );
// Job for the first bar
auto job1 = [&bars]() {
for (int i = 0; i <= 100; i ) {
bars.for_one(0, updater{}, i);
sleep_for( milliseconds( 100 ) );
}
cout << endl;
};
// Job for the second bar
auto job2 = [&bars]() {
for (int i = 5; i <= 25; i ) {
bars.for_one(1, updater{}, i);
sleep_for(std::chrono::milliseconds(200));
}
cout << endl;
};
// Job for the third bar
auto job3 = [&bars]() {
for (float i = 0.1f; i <= 12.1f; i = 0.1f) {
bars.for_one(2, updater{}, i);
sleep_for(std::chrono::milliseconds(60));
}
cout << endl;
};
thread first_job(job1);
thread second_job(job2);
thread third_job(job3);
first_job.join();
second_job.join();
third_job.join();
The problem is that when I run the code, progress bars are printed overlapped, like this:
0 [00:53] gianluca@ubuntu:~/osmanip (main)$ ./bin/main.exe
[## ] 9.166668%
Instead I want something like:
0 [00:53] gianluca@ubuntu:~/osmanip (main)$ ./bin/main.exe
[############ ] 45%
[######### ] 39%
[################## ] 72%
I found in this example that a solution may be the one of using an std::atomic<bool> variable in the MultiProgressBar class and when it is True move the cursor up, else doesn't do anything, like in this example, from the link I put (this is the definition of a method of a MultiProgressBar class to call the update methods, here called write_progress, of the other single progress bars):
public:
//Some code...
//...
void write_progress(std::ostream &os = std::cout) {
std::unique_lock lock{mutex_};
// Move cursor up if needed
if (started_)
for (size_t i = 0; i < count; i)
os << "\x1b[A";
// Write each bar
for (auto &bar : bars_) {
bar.get().write_progress();
os << "\n";
}
if (!started_)
started_ = true;
}
//Some code...
//...
private:
// [...]
std::mutex mutex_;
std::atomic<bool> started_{false};
However I don't understand where and how I should put an std::atomic<bool> variable inside my MultiProgressBar class. Sorry, but I am still learning how to use threads. Can someone help me?
CodePudding user response:
The problem is that each bar erases the line, but doesn't update its row before doing it, so all just erase the same line every time. In the post you linked, the MultiBar class is in charge of erasing and writing, so yours must do the same. As in your case each bar erases and writes itself from the for_one method, your MultiProgressBar must at least update the cursor of the row before calling the update of the bar. Here's the pseudocode to do it:
updateOneProgressBar(barIndex){
rowDiff = lastUpdatedBarIndex - barIndex
if(rowDiff < 0) // move upwards
moveCursorUp(-rowDiff)
else // move downwards
moveCursorDown(rowDiff)
// now the cursor is in the correct row
eraseAndRewriteBar(barIndex)
// don't forget to update the control variable
lastUpdatedBarIndex = barIndex
}
CodePudding user response:
You cannot do this without using a console graphics library of some kind, because there is no way to move the cursor up to write to a previous line. You will need something like curses, or the Win32 console APIs like SetConsoleCursorPos and friends.
