Search code examples
c++multithreadingc++11initialization

Knowing std::once_flag is used without invoking std::call_once (Set once_flag after task finishes successfully)


Edit: this is an XY Problem but keep the original title as well. It might help other people having the same XY problem. The goal should be: "have std::once_flag flipped only after the task is successfully finished."

Use case

  1. Initialization needs to be done just once and it might be called concurrently.
  2. Before calling the initialization, validation needs to be done.
  3. If it's already initialized, validation is not required.

Question

How do I not using g_init(in the below sample) while avoiding the filesystem::exists() check after initialization?

Restriction

  1. I cannot modify caller which is a infrastructure that I cannot change.
  2. cfg["PATH"] is unknown and it can only be supplied by caller.

Welcome C++20 or C++23 and other solutions. It's good to be standard or homemade solution. Also it might be an XY problem...

A simplified demo

#include <mutex>
#include <filesystem>
#include <atomic>
#include "fmt/core.h"
#include "nlohmann/json.hpp"
using json = nlohmann::json;
std::once_flag g_flag;
std::atomic<bool> g_init{false};

void doOnce(std::string path){
    // initializing from file...
    fmt::print("initialization done");
}

void doWork(json& cfg){
    // Atomic flag for 
    if(g_init) {
        fmt::print("already inited");
        return;
    }

    std::string path = cfg["PATH"];
    if(!std::filesystem::exists(path)){
        fmt::print("load failed");
        return;
    }
    std::call_once(g_flag, doOnce, path);
}

int main() {
    json cfg;
    cfg["PATH"] = "/opt/usr/foo";

    // In the real world case, caller will call it
    // with random values and PATH might not exist.
    doWork(cfg);
}

It seems that std::once_flag doesn't expose anything to know whether it's called.


Solution

  • This is exactly the problem call_once is supposed to solve. If you let it do it's job it'll handle all this for you. If you throw an exception when initialisation fails call_once will run again:

    void doOnce(std::string path){
        if(!std::filesystem::exists(path)){
            throw std::runtime_error("load failed");
        }
        // initializing from file...
        fmt::print("initialization done");
    }
    
    void doWork(json& cfg){
        std::string path = cfg["PATH"];
        try
        {
            std::call_once(g_flag, doOnce, path);
        }
        catch (std::exception& ex)
        {
            fmt::print(ex.what());
        }
    }