如果我有8个线程,并且数组中有1,000,000,000个元素,那么我可以有1,000,000,000个mutices,其中索引表示被锁定和写入的数组中的元素。但是,这对我来说相当浪费,并且需要大量内存。
有没有办法我只能使用8个mutes并具有相同的功能?
答案 0 :(得分:4)
在这里大声想一下...不确定是否有效率,但是:
您可以创建锁定某些索引的方法:
vector<int> mutexed_slots;
std::mutex mtx;
bool lock_element(int index)
{
std::lock_guard<std::mutex> lock(mtx);
// Check if item is in the mutexed list
if ( !std::find(mutexed_slots.begin(), mutexed_slots.end(), index) != vector.end() )
{
// If its not then add it - now that array value is safe from other threads
mutexed_slots.emplace_back(index);
return true;
}
return false;
}
void unlock_element(int index)
{
std::lock_guard<std::mutex> lock(mtx);
// No need to check because you will only unlock the element that you accessed (unless you are a very naughty boy indeed)
vec.erase(vec.begin() + index);
}
注意:这是一个想法的开始,所以不要太猛烈地敲打它!它也是未经测试的伪代码。它并不是真正的最终答案-而是起点。请添加评论以改善或建议这样做是合理的。
更多点:
lock_element()
,直到它返回true为止。这种机制可以改进。但是作为一个概念-可行吗?我想,如果您需要真正快速的访问权限(也许您需要这样做),可能会效率不高吗?
更新
如果每个线程/工作人员在mutexed_slots
中“注册”自己的条目,则可以提高效率。这样,向量中就不会有push_back / remove了(开始/结束处除外)。因此,每个线程仅设置其已锁定的索引-如果没有锁定,则将其设置为-1(或类似值)。我认为还有很多这样的效率改进措施。再次,一个完整的类可以为您完成所有操作,这就是实现它的方法。
我为此安装了一个测试器,只是因为我很喜欢这种东西。我的实现是here
我认为这是一个公开的github回购-因此,欢迎您来看看。但是我将结果发布到了顶级自述文件上(所以稍微滚动一下就可以看到它们)。我实现了一些改进,例如:
因为我不依赖std :: atomic索引,所以不需要lock_guard进行“解锁”。
以下是我摘要的打印输出:
当工作量为1毫秒(执行每个动作所需的时间)时,完成的工作量为:
8117正常
注意,这些值各不相同,有时正常值更高,没有明显的赢家。
当工作量为0ms(基本上增加几个计数器)时,完成的工作量为:
因此,在这里您可以看到使用互斥保护将工作速度降低了大约三分之一(1/3)。测试之间的比率是一致的。
我还对1名工人进行了相同的测试,并且大致采用了相同的比率。但是,当我使数组更小(〜1000个元素)时,工作量为1ms时,完成的工作量仍然大致相同。但是当工作量很轻时,我得到的结果如下:
39157931
慢了大约7倍。
看来,锁定通常只会增加一个慢2-3倍的开销,然后再增加几个计数器。这可能是由于实际冲突而造成的,因为(从结果中)记录的最长锁定时间是40毫秒之久-但这是在工作时间非常快的情况下,所以发生了很多冲突(每次冲突成功锁定8次)。 / p>
答案 1 :(得分:1)
这取决于访问模式,您是否有办法有效地划分工作?基本上,您可以将数组划分为8个块(或尽可能多的块),并用互斥锁覆盖每个部分,但是如果访问模式是随机的,则仍然会有很多冲突。
您的系统上是否有TSX支持?这将是一个经典的示例,只有一个全局锁,并且除非实际发生冲突,否则线程将忽略它。
答案 2 :(得分:1)
您可以编写一个类,该类将在特定索引需要它时动态创建锁,std::optional
将对此有所帮助(前面的C ++ 17代码):
class IndexLocker {
public:
explicit IndexLocker(size_t size) : index_locks_(size) {}
std::mutex& get_lock(size_t i) {
if (std::lock_guard guard(instance_lock_); index_locks_[i] == std::nullopt) {
index_locks_[i].emplace();
}
return *index_locks_[i];
}
private:
std::vector<std::optional<std::mutex>> index_locks_;
std::mutex instance_lock_;
};
您还可以使用std::unique_ptr
来最小化堆栈空间,但保持相同的语义:
class IndexLocker {
public:
explicit IndexLocker(size_t size) : index_locks_(size) {}
std::mutex& get_lock(size_t i) {
if (std::lock_guard guard(instance_lock_); index_locks_[i] == nullptr) {
index_locks_[i] = std::make_unique<std::mutex>();
}
return *index_locks_[i];
}
private:
std::vector<std::unique_ptr<std::mutex>> index_locks_;
std::mutex instance_lock_;
};
使用此类不一定意味着您需要创建所有1,000,000元素。您可以使用模运算将储物柜视为互斥体的“哈希表”:
constexpr size_t kLockLimit = 8;
IndexLocker index_locker(kLockLimit);
auto thread_code = [&](size_t i) {
std::lock_guard guard(index_locker.get_lock(i % kLockLimit));
// Do work with lock.
};
值得一提的是,“哈希表”方法使死锁非常容易(例如,{get_lock(0)
之后是get_lock(16)
)。但是,如果每个线程一次只对一个元素起作用,那应该不是问题。
答案 3 :(得分:0)
还有其他一些需要进行细粒度锁定的折衷方案。原子操作很昂贵,因此锁定每个元素的并行算法所花的时间可能比顺序版本要长。
如何有效锁定取决于。数组元素是否依赖于数组中的其他元素?您主要在读书吗?主要是写作?
我不想将数组分成8个部分,因为这会导致 等待的可能性很高(访问是随机的)。的要素 数组是我将要编写的一个类,它将被多个Golomb编码 值。
我不认为拥有8个互斥锁是解决问题的方法。如果给定的锁保护了一个数组节,那么在并行执行过程中,如果不引入竞争条件(使互斥体毫无意义),则无法切换它来保护另一个节。
数组项很小吗?如果可以将它们缩减为8个字节,则可以使用alignas(8)
声明类并实例化std::atomic<YourClass>
对象。 (大小取决于体系结构。验证is_lock_free()
返回true。)这可能会打开无锁算法的可能性。危险指示器的变体似乎在这里很有用。这很复杂,所以如果时间有限,最好研究其他并行方法。