Threadsafe, ordered mapping / hash in C ++?

What is the best way to implement stream ordered (note1) mapping / hash in C ++? Aka, a fast-growing data structure (aka, not a queue), with which different flows can pass, sometimes inserting or deleting elements without interfering with the activity of other flows?

  • std :: map is not thread safe, and its operations are non-atomic - although only erasing cancels iterators

  • Wrapping each function in the entire map class does not fix the problem - you may have free iterators pointing to a node that is being erased by another thread. It should either block or prevent deletion until the current thread is the only one referencing it, or use the "saggy but still valid delete link" file system UNIX style

  • tbb :: concurrent_hash_map is designed for thread safety, but its iterators are still invalid when deleting their key. Even if the value is a smart pointer, the data will not be saved, since the link in the iterator will be lost.
  • You can use tbb: concurrent_hash_map by iterating through the keys instead of iterators (you can search for the key as O (1) rather than O (log N) like std :: map), but since it is a hash, it lacks order-specific ones like upper_bound, lower_bound, etc. Critically, if the key is left dangling by erasing in another thread, there is no obvious way to say that the code will return to the closest element to this key in the hash.
  • std :: unordered_map has the ability so that you can try to set the oath on the nearest element if your key is deleted through the bucket access functions. But std :: unordered_map is not considered thread safe (although it could potentially be hooked as thread safe). tbb :: concurrent_hash_map is thread safe (subject to the above restrictions), but for this, sufficient access to the buckets is not allowed.
  • std:: map, , , . O (log N) O (1).
  • , , std:: map . . , , , .
  • - , , , std:: map ( ), , , /.

, ?

** 1: "", , " , ", " ". , , , ( ). , , , , . /...

** 2: . ... std:: map ? , (, std:: shared_ptr), , , //. , " : ". A) ( = 0), , (operator =, operator ++, operator-- ..) , ; B) ( = false), . , , , .

, , ( //), , , , , . - ?

+4
3

- tbb::concurrent_unordered_map, - . , -, . , .

, , , : / .

, , . , , , , tbb::concurrent_hash_map find & insert. . :

for(iterator = table.begin(); iterator != table.end(); iterator++ ) {
    accessor acc;
    // a key cannot be changed thus it is safe to read it without lock
    table.find( acc, iterator->first );   // now get the get the lock
    if( acc->second.market_for_deletion )
        table.erase( acc );               // erase only by accessor
}

2, concurrent_hash_map, ( ), ( ).

( ) , - -, RW -lock tbb::spin_rw_mutex tbb::concurrent_unordered_map. , , , . , , . . -:

class concurrent_hash_table_with_erase_and_traverse {
    tbb::concurrent_unordered_map my_map;
    tbb::spin_rw_mutex            my_lock; // acquired as writer for cleanup only
    tbb::atomic<size_t>           my_trash_count; // indicates # of items for erase

public:
    void init_thread_for_concurrent_ops() { my_lock.lock_read(); }
    void release_thread()                 { my_lock.unlock(); } // assuming reader lock
    mapped_type read(key_type k) {
        // assert: under read lock (thread is initialized)
        if(my_trash_count > threshold) {  // time to remove items
            my_lock.unlock(); // release reader
            // waiting all the threads to enter this container
            // TODO: re-implement with try_lock and checking the condition 
            my_lock.lock();   // acquire writer

            if(my_trash_count > threshold) { // double-check
                my_trash_count = 0;
                for( auto it = my_map.begin(); it != my_map.end(); ) {
                    auto _it = it++;
                    if( _it->is_marked_for_erase )
                        my_map.unsafe_erase( _it );
                }
            }
            my_lock.unlock();    // release writer
            my_lock.lock_read(); // acquire reader
        }
        return my_map[k]; // note: access is not protected like in concurrent_hash_map
    }
    void safe_erase(key_type k) {
        // assert: under read lock
        my_map[k].is_marked_for_erase = true;
        my_trash_count++;
    }
};
+2

, , , , .

template<typename kType, typename dType>
class Locked {
  std::mutex mut;
  std::map<kType, dType> theMap; // change types as required
public:
  const dType get(const kType& key) const {
    std::lock_guard<std::mutex> g(mut);
    auto it = theMap.find(key);
    if (it != theMap.end())
      return *it;
    // throw or return buggy dType
    return dType(-1); // or whatever
  }
  void set(const kType& key, const dType& data) {
    std::lock_guard<std::mutex> g(mut);
    theMap[key] = data;
  }
  void delete(const kType& key) {
    std::lock_guard<std::mutex> g(mut);
    auto it = theMap.find(key);
    if (it != theMap.end()) {
      theMap.erase(it);
      return;
    }
    // throw?
  }
}

, , dType , shared_ptr.

.

, , / , tbb, .

++ 14 std::shared_timed_mutex, .

++ 17 std::shared_mutex, .

, ..

- .

0

Okay, so it took a long time ... and so much work that I decided to make a github project out of it;) But I finally have a class of thread-safe cards. Complete with a full set of tests and many customizable parameters. Hopefully if someone else needs this, they will use it!

https://github.com/KarenRei/safe-map#

0
source

Source: https://habr.com/ru/post/1613260/


All Articles