c++multithreadinglambdac++17

How to forward a lambda function to an underlying std::thread constructor in order to execute it in a separate thread?


Given a path, a file watcher is routinely checking its availability (in case of creation/deletion) or timestamp (in case the file exists and its contents have been modified). The code below (take from here) is work in progress and I am adapting it to also support single file (the original code supports multiple where the given path is a directory).

class FileWatcher {
public:
    std::string m_path;
    std::chrono::duration<int, std::milli> m_delay;
    std::unique_ptr<std::thread> m_watcher_thread;

    FileWatcher(std::string path, std::chrono::duration<int, std::milli> delay);

    void start(const std::function<void(std::string, FileStatus)>& action);
private:
    std::unordered_map<std::string, std::filesystem::file_time_type> m_paths;
    bool m_running = true;

    bool contains(const std::string& key);
};

FileWatcher::FileWatcher(std::string path, std::chrono::duration<int, std::milli> delay) : m_path{ path }, m_delay{ delay } {
    // std::this_thread::sleep_for(delay);
    auto const p = std::filesystem::path(m_path);
    if (std::filesystem::is_regular_file(p))
    {
        m_paths[p.string()] = std::filesystem::last_write_time(p);
    }
    else if (std::filesystem::is_directory(p))
    {
        for (auto const& file : std::filesystem::recursive_directory_iterator(m_path)) {
            m_paths[file.path().string()] = std::filesystem::last_write_time(file);
        }
    }
    else
    {
        throw std::exception("Given path to watch is expected to be a regular file or directory");
    }
}

bool FileWatcher::contains(const std::string& key)
{
    auto el = m_paths.find(key);
    return el != m_paths.end();
}

void FileWatcher::start(const std::function<void(std::string, FileStatus)>& action) {
    m_watcher_thread = std::unique_ptr<std::thread>(new std::thread(&action));
    while (m_running) {
       ... // WIP Check status of file and call action
    }
}

I initialize my watcher as follows:

config_watcher = std::shared_ptr<FileWatcher>(new FileWatcher{ "./config.json" , std::chrono::milliseconds(5000) });

and start it by calling (logger_spd is an std::shared_ptr to spdlog logger using a file sink)

config_watcher->start([](std::string path, FileStatus status) -> void {
    // Process only regular files, all other file types are ignored
    if (!std::filesystem::is_regular_file(std::filesystem::path(path)) && status != FileStatus::erased) {
        return;
    }
    switch (status) {
    case FileStatus::created:
    {
        std::lock_guard<std::mutex> guard(config_update_mutex);
        logger_spd->info("Configuration created");
        break;
    }
    case FileStatus::modified:
    {
        std::lock_guard<std::mutex> guard(config_update_mutex);
        logger_spd->info("Configuration modified");
        break;
    }
    case FileStatus::erased:
    {
        std::lock_guard<std::mutex> guard(config_update_mutex);
        logger_spd->info("Configuration deleted");
        break;
    }
    default:
        std::lock_guard<std::mutex> guard(config_update_mutex);
        logger_spd->error("Unknown file status");
    }

    std::this_thread::sleep_for(config_watcher->m_delay);
    }
);

The initial code is sub-optimal as mentioned in at least on of the comments below the article I have used as a source because of, among other issues, the std::this_thread::sleep_for() call inside the FileWatcher::start() that blocks the rest of the program unless instantiated in a separate thread. That is why I am trying to modify the code to act more as a typical worker thread, meaning that I would like to pass the action function (argument of FileWatcher::start()) to an underlying thread constructor and then manage everything inside the class.

I am having a problem to wrap my head around the forwarding the lambda to the underlying thread's constructor through the FileWatcher::start() function.


Solution

  • You should not start the thread with the action, instead you should have a different function that contains the loop and which calls the action inside the loop and use that function for the thread.

    Perhaps something like this:

    void FileWatcher::start(const std::function<void(std::string, FileStatus)>& action) {
        m_watcher_thread = std::thread([action]() {
            while (m_running) {
                // Wait until something happens...
                action(file, status);
            }
        });
    }
    

    Note that I don't use a pointer for the thread object, as that's not needed.

    The "sleep" should also be inside the loop, and not in the action function.