Search code examples
c++c++11thread-safetystdmapstdatomic

Thread-safety about `std::map<int, std::atomic<T>>` under a special condition


In general, it's not thread-safe to access the same instance of std::map from different threads.

But is it could be thread-safe under such a condition:

  1. no more element would be added to\remove from the instance of std::map after it has been initialized
  2. the type of values of the std::map is std::atomic<T>

Here is the demo code:

#include<atomic>
#include<thread>
#include<map>
#include<vector>
#include<iostream>

class Demo{
public:
Demo()
{
    mp_.insert(std::make_pair(1, true));
    mp_.insert(std::make_pair(2, true));
    mp_.insert(std::make_pair(3, true));
}

int Get(const int& integer, bool& flag)
{
    const auto itr = mp_.find(integer);
    if( itr == mp_.end())
    {
        return -1;
    }
    else
    {
        flag = itr->second;
        return 0;
    }
}
int Set(const int& integer, const bool& flag)
{
    const auto itr = mp_.find(integer);
    if( itr == mp_.end())
    {
        return -1;
    }
    else
    {
        itr->second = flag;
        return 0;
    }
}

private:
std::map<int, std::atomic<bool>> mp_;
};

int main()
{
    Demo demo;

    std::vector<std::thread> vec;

    vec.push_back(std::thread([&demo](){
        while(true)
        {
            for(int i=0; i<9; i++)
            {
                bool cur_flag = false;
                if(demo.Get(i, cur_flag) == 0)
                {
                    demo.Set(i, !cur_flag);
                }
                std::this_thread::sleep_for(std::chrono::milliseconds(1000));
            }
        }
    }));

    vec.push_back(std::thread([&demo](){
        while(true)
        {
            for(int i=0; i<9; i++)
            {
                bool cur_flag = false;
                if(demo.Get(i, cur_flag)==0)
                {
                    std::cout << "(" << i << "," << cur_flag <<")" << std::endl;
                }
                std::this_thread::sleep_for(std::chrono::milliseconds(10));
            }
        }
    })
    );

    for(auto& thread:vec)
    {
        thread.join();
    }
}

What more, the compiler does not complain about anything with -fsanitize=thread option.


Solution

  • Yes, this is safe.

    Data races are best thought of as unsynchronized conflicting access (potential concurrent reads and writes).

    std::thread construction imposes an order: the actions which preceded in code are guaranteed to come before the thread starts. So the map is completely populated before the concurrent accesses.

    The library says standard types can only access the type itself, the function arguments, and the required properties of any container elements. std::map::find is non-const, but the standard requires that for the purposes of data races, it is treated as const. Operations on iterators are required to at most access (but not modify) the container. So the concurrent accesses to std::map are all non-modifying.

    That leaves the load and store from the std::atomic<bool> which is race-free as well.