6

Microsoft 的 Parallel.For 文档包含以下方法:

static void MultiplyMatricesParallel(double[,] matA, double[,] matB, double[,] result)
{
    int matACols = matA.GetLength(1);
    int matBCols = matB.GetLength(1);
    int matARows = matA.GetLength(0);

    // A basic matrix multiplication.
    // Parallelize the outer loop to partition the source array by rows.
    Parallel.For(0, matARows, i =>
    {
        for (int j = 0; j < matBCols; j++)
        {
            double temp = 0;
            for (int k = 0; k < matACols; k++)
            {
                temp += matA[i, k] * matB[k, j];
            }
            result[i, j] = temp;
        }
    }); // Parallel.For
}

matA在此方法中,可能有多个线程从和读取值matB,这些值都是在调用线程上创建和初始化的,并且可能有多个线程将值写入result,稍后由调用线程读取。在传递给 的 lambda 中Parallel.For,数组读取和写入没有显式锁定。因为这个例子来自微软,所以我认为它是线程安全的,但我试图了解幕后发生的事情以使其成为线程安全的。

据我所读的内容和我在 SO 上提出的其他问题(例如这个),据我所知,需要几个内存屏障才能使这一切正常工作。那些是:

  1. 创建和初始化matAand后调用线程上的内存屏障matB
  2. matA在从和读取值之前,每个非调用线程上的内存屏障matB
  3. 在将值写入 后,每个非调用线程上的内存屏障result,以及
  4. 在从 读取值之前调用线程上的内存屏障result

我是否正确理解了这一点?

如果是这样,是否Parallel.For会以某种方式完成所有这些工作?我去挖掘参考源,但在遵循代码时遇到了麻烦。我没有看到任何lock阻塞或MemoryBarrier呼叫。

4

4 回答 4

6

由于已经创建了数组,因此对其进行写入或读取不会导致任何大小调整。此外,代码本身会阻止读取/写入数组中的相同位置。

底线是代码始终可以计算在数组中读取和写入的位置,并且这些调用永远不会相互交叉。因此,它是线程安全的。

于 2017-01-14T19:09:56.187 回答
6

您正在寻找的内存屏障位于任务调度程序中。

ParallelFor 将工作分解为任务,然后一个工作窃取调度程序执行这些任务。工作窃取调度程序所需的最小内存屏障是:

  1. 创建任务后的“释放”栅栏。
  2. 任务被盗时的“获取”围栏。
  3. 当被盗任务完成时的“释放”栅栏。
  4. 等待任务的线程的“获取”栅栏。

在此处查找用于将任务排入队列的原子(“互锁”)操作所隐含的 1 的位置。看这里2 在任务被盗时被原子操作、易失性读取和/或锁暗示的地方。

我一直无法找到 3 和 4 的位置。3 和 4 可能由某种原子连接计数器暗示。

于 2017-01-15T15:31:14.693 回答
3

在线程内部(实际上是:任务)对 matA 和 matB 的访问是只读的,结果是只写的。

并行读取本质上是线程安全的,写入是线程安全的,因为i每个任务的变量都是唯一的。

在这段代码中不需要内存屏障(除了在整个 Parallel.For 之前/之后,但可以假设)。

从您编号的项目中,
1) 和 4) 隐含在 Parallel.For() 中
,2) 和 3) 根本不需要。

于 2017-01-14T20:49:34.627 回答
2

我认为您对记忆障碍的想法印象深刻,但我真的无法理解您的担忧。让我们看一下您调查过的代码:

  1. 在主线程中启动并填充了 3 个数组。所以这就像你给一个变量赋值并调用一个方法——CLR 确保你的方法为参数获取新的值。如果初始化是在后台和/同时由其他线程完成的,这里可能会出现问题。在这种情况下,你是对的,你需要一些同步结构,内存屏障或lock语句或其他技术。

  2. 用于并行执行的代码从0to获取所有值matARows并为它们创建一个任务数组。您需要了解并行化代码的两种不同方法:通过操作和通过数据。在这里,我们有多个行,它们具有相同的 lambda 操作。变量的赋值temp不是共享的,因此它们是线程安全的,并且不需要内存屏障,因为没有旧值和新值。同样,首先,如果其他一些线程更新初始矩阵,您需要一个同步结构。

  3. Parallel.For确保所有任务都已完成(运行到完成、被取消或出错),直到它继续执行下一条语句,因此循环内的代码将作为正常方法执行。你为什么不需要这里的屏障?因为所有的写操作都是在不同的行上进行的,它们之间没有交集,所以是数据并行。但是,与其他情况一样,如果其他线程需要来自某些循环迭代的新值,您仍然需要同步。所以这段代码是线程安全的,因为它在数据上是几何并行的,并且不会产生竞争条件。

这个例子很简单,真正的算法一般需要比较复杂的逻辑。您可以研究各种方法来证明代码是线程安全的,而无需使用同步来使代码无锁。

于 2017-01-14T22:30:17.867 回答