3

Consider the following serial function. When I parallelize my code, every thread will call this function from within the parallel region (not shown). I am trying to make this threadsafe and efficient (fast).

float get_stored_value__or__calculate_if_does_not_yet_exist( int A )
{    
    static std::map<int, float> my_map;

    std::map::iterator it_find = my_map.find(A);  //many threads do this often.

    bool found_A =   it_find != my_map.end();

    if (found_A)
    {
        return it_find->second;
    } 
    else
    {
      float result_for_A = calculate_value(A);  //should only be done once, really.
      my_map[A] = result_for_A;
      return result_for_A;
    }    
}

Almost every single time this function is called, the threads will successfully "find" the stored value for their "A" (whatever it is). Every once in a while, when a "new A" is called, a value will have to be calculated and stored.

So where should I put the #pragma omp critical ?

Though easy, it is very inefficient to put a #pragma omp critical around all of this, since each thread will be doing this constantly and it will often be the read-only case.

Is there any way to implement a "one-way" critical, or a "one-way" lock routine? That is, the above operations involving the iterator should only be "locked" when writing to my_map in the else statement. But multiple threads should be able to execute the .find call simultaneously.

I hope I make sense. Thank you.

cmo
  • 3,762
  • 4
  • 36
  • 64

3 Answers3

2

According to this link on Stack Overflow inserting into an std::map doesn't invalidate iterators. The same goes for the end() iterator. Here's a supporting link.

Unfortunately, insertion can happen multiple times if you don't use a critical section. Also, since your calculate_value routine might be computationally expensive, you will have to lock to avoid this else clause being operated on twice with the same value of A and then inserted twice.

Here's a sample function where you can replicate this incorrect multiple insertion:

void testFunc(std::map<int,float> &theMap, int i)
{
    std::map<int,float>::iterator ite = theMap.find(i);

    if(ite == theMap.end())
    {
         theMap[i] = 3.14 * i * i;
     }
}

Then called like this:

std::map<int,float> myMap;

int i;
#pragma omp parallel for
for(i=1;i<=100000;++i)
{
    testFunc(myMap,i % 100);
}

if(myMap.size() != 100)
{
    std::cout << "Problem!" << std::endl;
}

Edit: edited to correct error in earler version.

Community
  • 1
  • 1
Chris A.
  • 6,817
  • 2
  • 25
  • 43
  • Is this true even if the `.insert` occurs "mid-find"? (i.e. while this thread is inside the `.find` call). – cmo May 09 '12 at 19:06
  • While the insertion operation may be *called* multiple times, the actually writing may occur once or more than once depending on *which insertion operator you use*. Using `operator[]`, e.g. `myMap[i] = 3.14*i*i` will result in multiple writes. However, `myMap.insert(std::pair(i, 3.14*i*i))` will actually write only one time ever. – cmo May 10 '12 at 12:44
1

While @ChrisA's answer may solve your problem, I'll leave my answer here in case any future searchers find it useful.

If you'd like, you can give #pragma omp critical sections a name. Then, any section with that name is considered the same critical section. If this is what you would like to do, you can easily cause onyl small portions of your method to be critical.

#pragma omp critical map_protect
{
    std::map::iterator it_find = my_map.find(A);  //many threads do this often.

    bool found_A =   it_find != my_map.end();
}

...

#pragma omp critical map_protect
{
    float result_for_A = calculate_value(A);  //should only be done once, really.
    my_map[A] = result_for_A;
}

The #pragma omp atomic and #pragma omp flush directives may also be useful.

atomic causes a write to a memory location (lvalue in the expression preceded by the directive) to always be atomic.

flush ensures that any memory expected to be available to all threads is actually written to all threads, not stored in a processor cache and unavailable where it should be.

Sam DeHaan
  • 10,246
  • 2
  • 40
  • 48
  • But your use of `critical` is the inefficiency I was worried about - multiple threads cannot simlutaneously read the map due to the `critical`. – cmo May 09 '12 at 19:15
  • The `atomic` is a good idea though. So do I only need to place an `atomic` and `flush` around the write region? - and the read region needs to directive? – cmo May 09 '12 at 19:16
  • @CycoMatto I know. I thought i had done something clever with `atomic` and `flush`, realized my mistake, and edited my answer. It provides info that may be useful to someone else who stumbles on your problem, but definitely doesn't provide the locked-if-writing behavior you want. – Sam DeHaan May 09 '12 at 19:16
  • @CycoMatto `atomic` only protects a statement, not a block, which is why my cleverness failed. `flush` would be put right before any attempted read (`.find`). – Sam DeHaan May 09 '12 at 19:17
1

OpenMP is a compiler "tool" for automatic loop parallelization, not a thread communication or synchronization library; so it doesn't have sophisticated mutexes, like a read/write mutex: acquire lock on writing, but not on reading.

Here's an implementation example.

Anyway Chris A.'s answer is better than mine though :)

Community
  • 1
  • 1
CharlesB
  • 86,532
  • 28
  • 194
  • 218