multithreading - Concurrent write to different buckets in unordered_map (C++)? -


c++ newbie here. i'm trying write different buckets concurrently in unordered_map. can tell searching, understanding should thread safe operation. (perhaps incorrect) understanding based on answers here , here, referenced portion of c++11 standard (particularly item 2 -- emphasis mine):

23.2.2 container data races [container.requirements.dataraces]

1 purposes of avoiding data races (17.6.5.9), implementations shall consider following functions const: begin, end, rbegin, rend, front, back, data, find, lower_bound, upper_bound, equal_range, @ and, except in associative or unordered associative containers, operator[].

2 notwithstanding (17.6.5.9), implementations required avoid data races when contents of contained object in different elements in same sequence, excepting vector<bool>, modified concurrently.

3 [ note: vector x size greater one, x[1] = 5 , *x.begin() = 10 can executed concurrently without data race, x[0] = 5 , *x.begin() = 10 executed concurrently may result in data race. exception general rule, vector < bool > y, y[0] = true may race y[1] = true. —end note ]

in case, seems writing different buckets not thread safe standard containers, demonstrated code below. you'll see enable lock corresponding bucket being modified before writing, yet pairs not recorded correctly. it's worth, if use single lock -- e.g., change auto bkt = mm->bucket(key); auto bkt=0;, locking entire unordered_map container -- works expected.

#include <iostream> #include <unordered_map> #include <atomic> #include <vector> #include <thread>  #define num_locks 409 #define n 100 #define num_threads 2  using namespace std;   class spinlock {     public:         void lock()         {             while(lck.test_and_set(memory_order_acquire)){}         }     void unlock()         {             lck.clear(memory_order_release);         }      private:         atomic_flag lck = atomic_flag_init; };   vector<spinlock> spinlocks(num_locks);   void add_to_map(unordered_map<int,int> * mm, const int keystart, const int keyend, const int tid){      for(int key=keystart;key<keyend;++key){         auto bkt = mm->bucket(key);          //lock bucket         spinlocks[bkt].lock();          //insert pair         mm->insert({key,tid});          //unlock bucket         spinlocks[bkt].unlock();     }  }   int main() {      int nbefore, nafter;     thread *t = new thread[num_threads];      //create unordered map, , reserve enough space avoid rehash     unordered_map<int,int> my_map;     my_map.reserve(2*num_threads*n);      //count number of buckets make sure rehash didn't occur     nbefore=my_map.bucket_count();       // launch num_threads threads.  thread k adds keys k*n through (k+1)*n-1 hash table, associated value = k.      for(int threadid=0;threadid<num_threads;++threadid){         t[threadid]=thread(add_to_map,&my_map,threadid*n,(threadid+1)*n,threadid);     }      // wait threads finish     for(int threadid=0;threadid<num_threads;++threadid){         t[threadid].join();     }      //count number of buckets make sure rehash didn't occur     nafter=my_map.bucket_count();       cout << "number of buckets before adding elements: " << nbefore <<endl;     cout << "number of buckets after  adding elements: " << nafter  << " <--- same above, rehash didn't occur" <<endl;      //see if keys missing     for(int key=0;key<num_threads*n;++key){          if(!my_map.count(key)){              cout << "key " << key << " not found!" << endl;          }     }      return 0; } 

the program exit when key erroneously not entered. sample output is:

number of buckets before adding elements: 401 number of buckets after  adding elements: 401 <--- same above, rehash didn't occur key 0 not found! key 91 not found! key 96 not found! key 97 not found! key 101 not found! key 192 not found! key 193 not found! key 195 not found! 

so, question two-fold:

  1. am doing wrong in how locking buckets?
  2. if so, there better way lock map on bucket-by-bucket basis enable concurrent writes different buckets?

finally, i'll mention tried tbb's concurrent_unordered_map, slower in application doing things in serial. stray errors aside, bucket-locking approach using std::unordered_map performed better.

the elements of container not buckets, rather value_type elements.

modifying 1 element in std container has no concurrency impact on other elements. modifying 1 bucket has no such guarantee.

adding or removing elements in bucket non-const operation on container not special list of non-const operations safe use without synchronization.


Comments

Popular posts from this blog

tcpdump - How to check if server received packet (acknowledged) -