必须使用互斥锁来"get"数组中的值吗?

must a mutex be used to "get" values from an array?

本文关键字:数组 get      更新时间:2023-10-16

我知道,如果我要从多个线程分配值到数组中的相同位置(或增加该值等),我需要使用互斥锁,以便数组中该部分的值保持一致。

(示例):

for(ix = 0; ix < nx; ix++)
{
  x = x_space[ix];
  for(iy = 0; iy < ny; iy++)
  {
    y = y_space[iy];
    mutex_lock[&mut];
    sum = sum + f(x,y);
    mutex_unlock[&mut];
  }
}

,但是否也有必要使用互斥锁周围的代码段,线程可能都是从数组获取的值?

(示例):

for(ix = 0; ix < nx; ix++)
{
  mutex_lock[&xmut];
  x = x_space[ix];
  mutex_unlock[&xmut];
  for(iy = 0; iy < ny; iy++)
  {
    mutex_lock[&ymut];
    y = y_space[iy];
    mutex_unlock[&ymut];
    mutex_lock[&mut];
    sum = sum + f(x,y);
    mutex_unlock[&mut];
  }
}

No。你可以这样想:很多人可以同时看一杯水,但一次只能有一个人喝。

只要你只是在阅读(复制或其他),它就可以。然而,如果你正在处理没有原子操作的数据类型(或者一些基本数据类型,由于内存对齐或其他原因,没有进行原子操作),并且其他人可能正在写入该内存,那么你可以查看处于"半更改"状态的数据块,其中其他人正在更改它。因此您可能需要一个互斥锁,这取决于您的情况。

答案是,这取决于…如果您的整数在大多数体系结构上正确对齐,您将获得原子读和写,因此不需要锁定。

但是,如果它们未对齐,则对它们的更新可能是非原子的,并且需要锁定。除非您能保证写操作是原子的(即,一条机器指令),否则我建议安全起见,锁定操作。

如果您绝对确定在读取时值不会改变,那么就不需要互斥锁。因此,如果只有读操作,则不需要互斥。

如果您的目标只是计算总和并单独存储,则不需要互斥锁。实际上,你的算法本质上是非常"顺序"的。实际上,您可以通过计算局部和并在最后进行聚合(农民-工人类型的问题)来完全避免互斥锁。

不需要使用互斥锁,只要你100%确定数组没有被其他线程调整大小或删除。

只要没有对读取的数组进行写/重新分配,就不需要锁定它们。

我还可以衷心建议使用不那么细粒度的锁吗?这将不会很好地执行,是我的猜测

基于OpenMP的样本
#pragma omp parallel for reduction (+:sum)
for(ix = 0; ix < nx; ix++)
{
    x = x_space[ix];
    for(iy = 0; iy < ny; iy++)
    {
        y = y_space[iy];
        sum += f(x,y);
    }
}
// sum is automatically 'collected' from the parallel team threads

这主要取决于您必须对该和做什么以及数组本身表示什么(以及和也表示什么)。一些数组元素在求和之后发生了变化(但仍然完成了求和),这是否可以接受?如果答案是,则不需要锁定。

如果答案是否定的,那么您必须在sum计算的整个过程中锁定整个数组,并且只有在sum完成其目的后才释放锁。

如果你正在使用OpenMP 3.1,有一个很棒的原子访问新特性,在这种情况下,你想要

#pragma omp atomic read
some_private_var = some_shared_var[some_index];

这里有两个好处,一个是隐含的同花顺,就像做

一样
#pragma omp flush(some_shared_var[some_index])

在读取之前,但是openmp不允许刷新未引用的值。虽然可以不使用列表刷新,但所有内容都会刷新,因此如果在某些计算的最内层循环中,这可能会很昂贵。

另一个好处当然是读操作的原子性质。注意,some_shared_var[some_index]可以是任意大小的(可能是c++中的结构体或某个对象)。如果其他线程想要写入this,例如,可以通过复制对象内部的每个基本数据来实现,它不能中断原子读。

就开销而言,对我来说这比锁要快得多,如果some_shared_var[some_index]是一个原始数据类型,那么读取可能会自动发生,但是现在我们得到了flush。

其他一些想法:

如果读取最近的值并不重要,那么可以不使用原子读取。这提供了从缓存值中读取更快的可能性(例如CPU寄存器)。只是要注意some_shared_var[some_index]是否是一个大对象,因为它可能会被另一个线程部分地写入。

我认为原子只读必须来自所有cpu都可以访问的内存中的某个地方,因此它仍然可以驻留在片上缓存(例如共享L3缓存),因此您不会被迫从DRAM中读取。我不是100%确定这总是正确的,但我已经确认了它为我自己的计算机通过计时一些实验,其中内存使用低于和高于我的CPU的片上缓存。