在我目前的情况下,速度至关重要我有一个只有多个线程读取的地图,这样可以正常工作。现在出现了一个要求,可能需要偶尔写入静态映射,而其他线程正在读取映射。我相信这是一个游戏规则改变者,因为我需要锁定我的地图以保证线程的安全。这造成了一个问题,因为我有多个线程10-12个线程将要读取地图。如果一张地图在地图上锁定(因为它的读数),我相信锁定是必要的,因为可能会将某些内容写入地图。无论如何,正如我之前所说,如果一张地图正在阅读,那么其他地图就不会像之前那样对地图进行并行读取访问。有什么办法可以绕过这个问题吗?
答案 0 :(得分:13)
您可以使用地图旁边的shared_mutex
获取共享或唯一访问权限。通常,写操作需要唯一访问,而读操作则需要共享访问。
只要没有线程持有唯一访问权限,任意数量的线程都可以获得共享访问权限。如果某个线程尝试获取唯一访问权限,则会等待所有共享访问权限释放。
标准库和Boost提供shared_lock<T>
和unique_lock<T>
,以便范围有限地获取shared_mutex
。
请注意,有些人声称shared_mutex
表现不佳,但我没有看到任何证据或强有力的分析来支持这些说法。如果对你很重要,可能值得研究。
答案 1 :(得分:7)
只是为了你的c ++乐趣,阅读这本书,你会发现WAY比花的钱更值钱,你的并发世界将开阔
C++-Concurrency in Action Practical Multithreading
这些书讨论了线程数据共享,如何唤醒线程,创建线程池等等之间的所有问题和实际解决方案...更多...等等
这里是一个在不使用atomic或shared_locks的情况下在线程之间共享数据的示例
template<class T>
class TaskQueue
{
public:
TaskQueue(){}
TaskQueue& operator = (TaskQueue&) = delete;
void Push(T value){
std::lock_guard<std::mutex> lk(mut);
data.push(value);
condition.notify_one(); //if you have many threads trying to access the data at same time, this will wake one thread only
}
void Get(T& value){
std::unique_lock<std::mutex> lk(mut);
condition.wait(lk, [this]{ return !data.empty(); }); // in this case it waits if queue is empty, if not needed you can remove this line
value = data.front();
data.pop();
lk.unlock();
}
private:
std::mutex mut;
std::queue<T> data; //in your case change this to a std::map
std::condition_variable condition;
};
答案 2 :(得分:5)
其中一个解决方案可能是保留指向该映射的指针,当您需要修改它时 - 制作副本,修改该副本,然后将指针原子交换到新实例。这个解决方案会消耗更多的内存,但是如果你有很多读取线程可能会更有效,因为这个方法是无锁的。
在下面的示例中,只有一个线程可以修改映射。这并不意味着一次一个线程,它意味着数据结构生命周期中的一个和相同的线程。否则,需要在保留保护updateMap
中的整个代码的互斥锁的同时进行修改。读者线程可以像往常一样访问theData
- 没有任何锁定。
typedef std::map<...> Data;
std::atomic<Data *> theData;
void updateMap( ... )
{
Data *newData = new Data( *theData );
// modify newData here
Data *old = theData.exchange( newData );
delete old;
}
答案 3 :(得分:4)
这是我在不使用stl容器的情况下执行线程安全通用可调整大小的hashmap:
#pragma once
#include <iomanip>
#include <exception>
#include <mutex>
#include <condition_variable>
/*
* wrapper for items stored in the map
*/
template<typename K, typename V>
class HashItem {
public:
HashItem(K key, V value) {
this->key = key;
this->value = value;
this->nextItem = nullptr;
}
/*
* copy constructor
*/
HashItem(const HashItem & item) {
this->key = item.getKey();
this->value = item.getValue();
this->nextItem = nullptr;
}
void setNext(HashItem<K, V> * item) {
this->nextItem = item;
}
HashItem * getNext() {
return nextItem;
}
K getKey() {
return key;
}
V getValue() {
return value;
}
void setValue(V value) {
this->value = value;
}
private:
K key;
V value;
HashItem * nextItem;
};
/*
* template HashMap for storing items
* default hash function HF = std::hash<K>
*/
template <typename K, typename V, typename HF = std::hash<K>>
class HashMap {
public:
/*
* constructor
* @mSize specifies the bucket size og the map
*/
HashMap(std::size_t mSize) {
// lock initialization for single thread
std::lock_guard<std::mutex>lock(mtx);
if (mSize < 1)
throw std::exception("Number of buckets ust be greater than zero.");
mapSize = mSize;
numOfItems = 0;
// initialize
hMap = new HashItem<K, V> *[mapSize]();
}
/*
* for simplicity no copy constructor
* anyway we want test how different threads
* use same instance of the map
*/
HashMap(const HashMap & hmap) = delete;
/*
* inserts item
* replaces old value with the new one when item already exists
* @key key of the item
* @value value of the item
*/
void insert(const K & key, const V & value) {
std::lock_guard<std::mutex>lock(mtx);
insertHelper(this->hMap, this->mapSize, numOfItems, key, value);
condVar.notify_all();
}
/*
* erases item with key when siúch item exists
* @key of item to erase
*/
void erase(const K & key) {
std::lock_guard<std::mutex>lock(mtx);
// calculate the bucket where item must be inserted
std::size_t hVal = hashFunc(key) % mapSize;
HashItem<K, V> * prev = nullptr;
HashItem<K, V> * item = hMap[hVal];
while ((item != nullptr) && (item->getKey() != key)) {
prev = item;
item = item->getNext();
}
// no item found with the given key
if (item == nullptr) {
return;
}
else {
if (prev == nullptr) {
// item found is the first item in the bucket
hMap[hVal] = item->getNext();
}
else {
// item found in one of the entries in the bucket
prev->setNext(item->getNext());
}
delete item;
numOfItems--;
}
condVar.notify_all();
}
/*
* get element with the given key by reference
* @key is the key of item that has to be found
* @value is the holder where the value of item with key will be copied
*/
bool getItem(const K & key, V & value) const {
std::lock_guard<std::mutex>lock(mtx);
// calculate the bucket where item must be inserted
std::size_t hVal = hashFunc(key) % mapSize;
HashItem<K, V> * item = hMap[hVal];
while ((item != nullptr) && (item->getKey() != key))
item = item->getNext();
// item not found
if (item == nullptr) {
return false;
}
value = item->getValue();
return true;
}
/*
* get element with the given key by reference
* @key is the key of item that has to be found
* shows an example of thread waitung for some condition
* @value is the holder where the value of item with key will be copied
*/
bool getWithWait(const K & key, V & value) {
std::unique_lock<std::mutex>ulock(mtxForWait);
condVar.wait(ulock, [this] {return !this->empty(); });
// calculate the bucket where item must be inserted
std::size_t hVal = hashFunc(key) % mapSize;
HashItem<K, V> * item = hMap[hVal];
while ((item != nullptr) && (item->getKey() != key))
item = item->getNext();
// item not found
if (item == nullptr) {
return false;
}
value = item->getValue();
return true;
}
/*
* resizes the map
* creates new map on heap
* copies the elements into new map
* @newSize specifies new bucket size
*/
void resize(std::size_t newSize) {
std::lock_guard<std::mutex>lock(mtx);
if (newSize < 1)
throw std::exception("Number of buckets must be greater than zero.");
resizeHelper(newSize);
condVar.notify_all();
}
/*
* outputs all items of the map
*/
void outputMap() const {
std::lock_guard<std::mutex>lock(mtx);
if (numOfItems == 0) {
std::cout << "Map is empty." << std::endl << std::endl;
return;
}
std::cout << "Map contains " << numOfItems << " items." << std::endl;
for (std::size_t i = 0; i < mapSize; i++) {
HashItem<K, V> * item = hMap[i];
while (item != nullptr) {
std::cout << "Bucket: " << std::setw(3) << i << ", key: " << std::setw(3) << item->getKey() << ", value:" << std::setw(3) << item->getValue() << std::endl;
item = item->getNext();
}
}
std::cout << std::endl;
}
/*
* returns true when map has no items
*/
bool empty() const {
std::lock_guard<std::mutex>lock(mtx);
return numOfItems == 0;
}
void clear() {
std::lock_guard<std::mutex>lock(mtx);
deleteMap(hMap, mapSize);
numOfItems = 0;
hMap = new HashItem<K, V> *[mapSize]();
}
/*
* returns number of items stored in the map
*/
std::size_t size() const {
std::lock_guard<std::mutex>lock(mtx);
return numOfItems;
}
/*
* returns number of buckets
*/
std::size_t bucket_count() const {
std::lock_guard<std::mutex>lock(mtx);
return mapSize;
}
/*
* desctructor
*/
~HashMap() {
std::lock_guard<std::mutex>lock(mtx);
deleteMap(hMap, mapSize);
}
private:
std::size_t mapSize;
std::size_t numOfItems;
HF hashFunc;
HashItem<K, V> ** hMap;
mutable std::mutex mtx;
mutable std::mutex mtxForWait;
std::condition_variable condVar;
/*
* help method for inserting key, value item into the map hm
* mapSize specifies the size of the map, items - the number
* of stored items, will be incremented when insertion is completed
* @hm HashMap
* @mSize specifies number of buckets
* @items holds the number of items in hm, will be incremented when insertion successful
* @key - key of item to insert
* @value - value of item to insert
*/
void insertHelper(HashItem<K, V> ** hm, const std::size_t & mSize, std::size_t & items, const K & key, const V & value) {
std::size_t hVal = hashFunc(key) % mSize;
HashItem<K, V> * prev = nullptr;
HashItem<K, V> * item = hm[hVal];
while ((item != nullptr) && (item->getKey() != key)) {
prev = item;
item = item->getNext();
}
// inserting new item
if (item == nullptr) {
item = new HashItem<K, V>(key, value);
items++;
if (prev == nullptr) {
// insert new value as first item in the bucket
hm[hVal] = item;
}
else {
// append new item on previous in the same bucket
prev->setNext(item);
}
}
else {
// replace existing value
item->setValue(value);
}
}
/*
* help method to resize the map
* @newSize specifies new number of buckets
*/
void resizeHelper(std::size_t newSize) {
HashItem<K, V> ** newMap = new HashItem<K, V> *[newSize]();
std::size_t items = 0;
for (std::size_t i = 0; i < mapSize; i++) {
HashItem<K, V> * item = hMap[i];
while (item != nullptr) {
insertHelper(newMap, newSize, items, item->getKey(), item->getValue());
item = item->getNext();
}
}
deleteMap(hMap, mapSize);
hMap = newMap;
mapSize = newSize;
numOfItems = items;
newMap = nullptr;
}
/*
* help function for deleting the map hm
* @hm HashMap
* @mSize number of buckets in hm
*/
void deleteMap(HashItem<K, V> ** hm, std::size_t mSize) {
// delete all nodes
for (std::size_t i = 0; i < mSize; ++i) {
HashItem<K, V> * item = hm[i];
while (item != nullptr) {
HashItem<K, V> * prev = item;
item = item->getNext();
delete prev;
}
hm[i] = nullptr;
}
// delete the map
delete[] hm;
}
};
答案 4 :(得分:0)
您需要的是Java中的ConcurrentHashMap,它允许并发读取和写入底层哈希表。此类是java.util.concurrent包的一部分,提供并发读取和写入(最高为并发级别,默认为16)。
您可以在javadoc中找到更多信息。我在这里引用javadoc:
一个哈希表,支持检索的完全并发和可更新的预期并发性。该类遵循与Hashtable相同的功能规范,并包括与Hashtable的每个方法相对应的方法版本。但是,即使所有操作都是线程安全的,检索操作也不需要锁定,并且不支持以阻止所有访问的方式锁定整个表。在依赖于线程安全但不依赖于其同步细节的程序中,此类可与Hashtable完全互操作。
答案 5 :(得分:0)
另外两个答案很好,但我想我应该添加一些颜色:
Cliff Click写了一个lock-free concurrent hash map in Java.将它改编为C ++(没有GC,不同的内存模型等)是非常重要的,但它是我见过的无锁数据结构的最佳实现。如果您可以使用JAva而不是C ++,那么这可能就是您的选择。
我不知道任何无锁的平衡二叉树结构。这并不意味着它们不存在。
最简单的方法是使用其他两个答案之一(批量复制/原子交换/类似shared_ptr
或读写器锁定)来控制对map
的访问。根据读写的相对数量和map
的大小,两者中的一个会更快;你应该进行基准测试,看看你应该使用哪一个。