jeudi 24 août 2023

Condition_variable wait_for timeout issue

I am trying to learn how to get conditional variables working with some threads, but am having some troubles. The goal is to get the main thread to spawn some 'tasks' for a set period of time which instantiates a Countdown object that includes a thread which uses the timeout of the condition_variable to know that the requested time has indeed lapsed before notifying the caller.

Each time I run the example below it gets a different result, sometimes erroring out, sometimes running, but the timeout for both child threads is shorter than requested (assuming it gets that far).

I appreciate that there are other ways to skin-the-cat to time threads and I could for instance use atomic_x or other means instead of a callback, but I just would like to see how to get the spirit of this implementation (i.e. conditional variable so I don't have to poll).

#include <chrono>
#include <condition_variable>
#include <functional>
#include <iostream>
#include <map>
#include <mutex>
#include <thread>

using Time = std::chrono::system_clock;
using Seconds = std::chrono::seconds;
using Timepoint = Time::time_point;

class Countdown {
private:
    Timepoint               target;
    std::thread             t;
    std::condition_variable cv;
    std::mutex              cv_m;
    unsigned int            guid;
    std::string             name;

public:
    Countdown() 
    { // Needed to compile, but doesn't appear to run 
        std::cout << "empty Countdown constructor" << std::endl;
    }

    Countdown(unsigned int guid_, std::string name_, unsigned int waitFor, std::function<void(unsigned int)> callback)
        : guid(guid_)
        , name(name_)
        , target(Time::now() + Seconds(waitFor))
    {

        auto exec_run = [this, guid_, waitFor, callback]() mutable {
            std::unique_lock<std::mutex> lk(cv_m);
            std::cout << "[Thread " << guid_ << "] waiting for " << waitFor << " seconds." << std::endl;
 
            Timepoint before = Time::now();
            if (cv.wait_until(lk, target) == std::cv_status::timeout)
            {
                Timepoint after = Time::now();
                std::chrono::duration<float> difference = after - before;
                std::cout << "[Thread " << guid_ << "] Elapsed " << difference.count() << " seconds." << std::endl;
                callback(guid_);
            } 
        };
        
        t = std::thread(exec_run);
    }

    Countdown(Countdown &&from) // move constructor
    {
        //std::cout << "Countdown move constructor" << std::endl; 
        target = from.target;
        t = std::move(from.t);
        name = from.name;
        guid = from.guid;
    }

    ~Countdown()
    {
        //std::cout << "~Countdown()" << std::endl; 
        if (t.joinable()) t.join();
    }
};

class Holder {
private:
    std::map<unsigned int, Countdown>   waitlist;
    unsigned int                        id;
    std::vector<unsigned int>           completed;

public:
    Holder()
        : id(0)
    { }

    // Create a new task with a name for WaitFor (s) period of time
    unsigned int addTask(std::string name, unsigned int waitFor) {
        id++;
        waitlist.emplace(std::pair(id, Countdown(id, name, waitFor, 
                                  std::bind(&Holder::taskComplete, this, std::placeholders::_1))));

        return id;
    }

    void taskComplete(unsigned int id)
    {
        std::cout << "[Thread " << id << "] taskComplete" << std::endl;
        // Add task id to the completed list to be picked up by main thread
        completed.push_back(id);
    }

    void cleanupCompleted()
    {
        // Purge the completed entries from the waitlist
        for (auto& id : completed)
        {
            std::cout << "[Main] Erasing task: " << id << std::endl;
            waitlist.erase(id);
        }

        // Empty the completed list
        completed.clear();
    }
};

int main()
{
    Holder *h = new Holder();
    // Create a task which spawns a thread, which notifies us when complete
    unsigned int id1 = h->addTask("fluffy", 1); // 1 second task
    unsigned int id2 = h->addTask("woof", 4);   // 4 second task
    std::cout << "[Main]: Done adding tasks.." << std::endl;
 
    // Rest a while..
    std::this_thread::sleep_for(Seconds(5));
    h->cleanupCompleted();

    // Just to show the main thread continues on.
    std::cout << "[Main]: Doing other stuff.." << std::endl;
    delete(h);

    return 0;
}

Actual / Expected Result:

[Main]: Done adding tasks..
[Thread 2] waiting for 4 seconds.
[Thread 1] waiting for 1 seconds.
[Thread 2] Elapsed 8.243e-06 seconds. **(This should be ~ 4 seconds)**
[Thread 2] taskComplete
[Thread 1] Elapsed 0.000124505 seconds. **(This should be ~1 second)**
[Thread 1] taskComplete
[Main] Erasing task: 2
[Main] Erasing task: 1
[Main]: Doing other stuff..

I 'think' the problem might be around the in-place addition of the 'Countdown' object to the map in the 'Holder' class. It needs a move constructor whereby I manually set each field (except the mutex and conditional_variable).

Any advice on how to do this correctly?

Aucun commentaire:

Enregistrer un commentaire