数组不同元素上的多个锁

时间:2019-02-09 18:46:08

标签: c++ multithreading mutex

如果我有8个线程,并且数组中有1,000,000,000个元素,那么我可以有1,000,000,000个mutices,其中索引表示被锁定和写入的数组中的元素。但是,这对我来说相当浪费,并且需要大量内存。

有没有办法我只能使用8个mutes并具有相同的功能?

4 个答案:

答案 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);
}

注意:这是一个想法的开始,所以不要太猛烈地敲打它!它也是未经测试的伪代码。它并不是真正的最终答案-而是起点。请添加评论以改善或建议这样做是合理的。

更多点:

  • 可能会使用更有效的STL
  • 您可能将所有这些与数据一起包装在一个类中
  • 您将需要遍历lock_element(),直到它返回true为止。这种机制可以改进。
  • 每个线程都需要记住他们当前正在使用哪个索引,以便它们仅解锁特定的索引-再次可以将其更多地集成到一个类中以确保行为。

但是作为一个概念-可行吗?我想,如果您需要真正快速的访问权限(也许您需要这样做),可能会效率不高吗?

更新

如果每个线程/工作人员在mutexed_slots中“注册”自己的条目,则可以提高效率。这样,向量中就不会有push_back / remove了(开始/结束处除外)。因此,每个线程仅设置其已锁定的索引-如果没有锁定,则将其设置为-1(或类似值)。我认为还有很多这样的效率改进措施。再次,一个完整的类可以为您完成所有操作,这就是实现它的方法。


测试/结果

我为此安装了一个测试器,只是因为我很喜欢这种东西。我的实现是here

我认为这是一个公开的github回购-因此,欢迎您来看看。但是我将结果发布到了顶级自述文件上(所以稍微滚动一下就可以看到它们)。我实现了一些改进,例如:

  • 运行时没有对保护阵列的插入/移除
  • 因为我不依赖std :: atomic索引,所以不需要lock_guard进行“解锁”。

    以下是我摘要的打印输出:

摘要:

当工作量为1毫秒(执行每个动作所需的时间)时,完成的工作量为:

  • 9808受保护
  • 8117正常

    注意,这些值各不相同,有时正常值更高,没有明显的赢家。

当工作量为0ms(基本上增加几个计数器)时,完成的工作量为:

  • 9791264 for受保护的
  • 29307829正常

因此,在这里您可以看到使用互斥保护将工作速度降低了大约三分之一(1/3)。测试之间的比率是一致的。

我还对1名工人进行了相同的测试,并且大致采用了相同的比率。但是,当我使数组更小(〜1000个元素)时,工作量为1ms时,完成的工作量仍然大致相同。但是当工作量很轻时,我得到的结果如下:

  • 5621311
  • 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。)这可能会打开无锁算法的可能性。危险指示器的变体似乎在这里很有用。这很复杂,所以如果时间有限,最好研究其他并行方法。