【问题标题】:How do I avoid large number of locks?如何避免大量锁?
【发布时间】:2021-08-03 13:47:31
【问题描述】:

arr 声明如下。

std::vector<std::vector<int>> arr(10,000,000);

我的串行代码类似于

for (const auto [x, y] : XY) {
  arr[x].push_back(y);
}

我使用openmp并定义了一个锁数组,如下所示。

std::vector<omp_lock_t> locks(10,000,000);

locks,我用

#pragma omp parallel for schedule(dynamic)
for (const auto [x, y] : XY) {
  omp_set_lock(&locks[x]);
  arr[x].push_back(y);
  omp_unset_lock(&locks[x]);
}    

这种方法适用于我的机器(windows linux 子系统)。但是后来我找到了以下帖子

c/c++ maximum number of mutexes allowed in Linux

这引起了我是否使用了太多锁的担忧,并且我的程序可能不适用于其他平台(允许的锁数量有限制的平台)。

我想知道是否还有另一种方式,我仍然具有与上述相同的控制粒度,并且它对允许的数量没有上限。我可以使用比较和交换之类的东西吗?

【问题讨论】:

  • 这真的取决于你想做什么。在最简单的情况下,您可以只使用std::atomic_int
  • 在“//更新a[i]”期间你做了什么?
  • 简单点。只有一把锁,你不喜欢什么?
  • 你为什么不使用std::mutexstd::condition_var?您可以决定为例如切片设置互斥锁。 1024 个元素。在 Linux 上,请参阅 futex(2)GNU libcMUSL libc 的源代码
  • 您在//update a[i] 中所做的事情对于这个问题至关重要。对我来说,单独锁定每个 int 似乎是一种严重的矫枉过正。是否有可能 2 个线程同时访问同一个值?

标签: c++ multithreading parallel-processing openmp mutex


【解决方案1】:

根据OpenMP documentationomp_lock_t代表“一个简单的锁”。我想这是某种自旋锁。因此,您不需要关心多个互斥体的限制。 (互斥体需要与内核/调度程序进行一些交互,这可能是限制其数量的原因。)

omp_lock_t 使用单个内存位置,这对于自旋锁来说很好。例如,这个现场演示显示 omp_lock_t 只占用 4 个字节,而 std::mutex 40 个字节:https://godbolt.org/z/sr845h8hx

请注意,自旋锁可以用单个字节实现,甚至可以用单个位(x86_64 上的BTS)。因此,您可以在内存中进一步压缩锁。但是,我会谨慎使用这种方法,因为它会带来严重的错误共享问题。

我认为您的解决方案非常好。由于临界区的操作应该非常快,并且多个线程同时访问同一个元素的可能性很小,所以我认为自旋锁是一个合适的解决方案。

编辑

正如 cmets 中所指出的,术语简单锁 可能并不意味着锁实际上是自旋锁。由 OpenMP 实现决定它将使用哪种类型的锁。然后,如果您想确定您使用的是真正的自旋锁(我仍然认为它是合适的),您可以自己编写或使用一些提供自旋锁的库。 (请注意,高效的自旋锁实现并不是那么简单。例如,它需要在 load/read 操作而不是 exchange/test-and-set 上进行自旋,这通常是幼稚的实现。)

【讨论】:

  • OpenMP 锁的实现完全取决于系统。标准中的“简单锁”仅仅意味着它不是递归(计数)锁,也不是读写锁。您不能假设 omp_lock_t 没有使用底层系统锁实现。 omp_lock_t 的大小也未指定。如果您切换到 LLVM,您将看到 sizeof(omp_lock_t) == sizeof(void *),因此其中可能存储了一个指针,该指针可以指向任意数量的存储空间。 “单个内存位置”也可能是锁表的索引...
  • @JimCownie 你是对的,我相应地更新了答案。我什至检查了 LLVM 的实现,似乎有一些条件编译可以选择锁的类型。
  • 不仅在编译时。查看令人羡慕的 KMP_LOCK_KIND。
【解决方案2】:

几种可能的解决方案

  1. 如果您使用的是支持事务同步扩展 (TSX) 的现代 Intel 处理器,则可以使用单个推测锁。 (请参阅 [您可能错过的两个小的 OpenMP 功能][1])。这显示了一个非常相似的用例。

  2. 您可以使用较小的锁数组并使用散列从数组索引映射到锁集。 像这样的东西(未经测试和未编译!)

    // Allocate and initialise the lock array, wherever you have that!
        enum { ln2NumLocks = 10,
               NumLocks = 1<<ln2NumLocks }
        omp_lock_t locks[NumLocks];
        for (int i=0; i<NumLocks; i++)
            omp_init_lock(&locks[i]);
    
    // Hash from array index to lock index. What you have here
    // probably doesn't matter too much. It doesn't need to be
    // a crypto-hash!
    int mapToLockIdx(int arrayIdx) {
        return ((arrayIdx >> ln2NumLocks) ^ arrayIdx) & (numLocks-1);
    }
    
    // Your loop is then something like this
    #pragma omp parallel for schedule(dynamic)
    for (const auto [x, y] : XY) {
        auto lock = &locks[mapToLock(x)]; 
        omp_set_lock(lock);
        arr[x].push_back(y);
        omp_unset_lock(lock);
    } 
    


  [1]: https://www.openmp.org/wp-content/uploads/SC18-BoothTalks-Cownie.pdf

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2013-06-07
    • 2012-08-14
    • 1970-01-01
    • 2015-01-07
    • 1970-01-01
    相关资源
    最近更新 更多