Parallel.For 中的内存障碍

Memory barriers in Parallel.For

Microsoft's documention of 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
}

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

根据我阅读的内容和我在 SO 上提出的其他问题(例如 )的理解,需要几个内存障碍才能使这一切正常进行。它们是:

  1. 创建和初始化 matAmatB
  2. 后调用线程上的内存屏障
  3. 每个非调用线程在从 matAmatB
  4. 读取值之前的内存屏障
  5. 在将值写入 result
  6. 后每个非调用线程上的内存屏障
  7. 在从 result.
  8. 读取值之前调用线程上的内存屏障

我的理解正确吗?

如果是这样,Parallel.For 会以某种方式完成所有这些吗?我深入研究了参考资料,但在遵循 the code 时遇到了麻烦。我没有看到任何 lock 块或 MemoryBarrier 调用。

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

最重要的是,代码始终可以计算数组中读取和写入的位置,并且这些调用永远不会相互交叉。因此,它是 thread-safe.

Threads(实际上:Tasks)内部访问matA和matB是read-only,结果是write-only。

并行读取本来就是thread-safe,写入是thread-safe,因为i这个变量对于每个Task都是唯一的。

这段代码不需要内存屏障(除了 before/after 整个 Parallel.For 但可以假设)。

形成你编号的项目,
Parallel.For()
隐含了 1) 和 4) 2) 和 3) 根本不需要。

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

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

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

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

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

您要查找的内存屏障在任务调度程序中。

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

  1. 创建任务后 "release" 围栏。
  2. 当任务被盗时 "acquire" 围栏。
  3. 一个 "release" 被盗任务完成时的围栏。
  4. 等待任务的线程的 "acquire" 栅栏。

查看 here for where 1 is implied by the atomic ("Interlocked") operations used to enqueue a task. Look here 其中 2 表示原子操作、易失性读取、and/or 任务被盗时锁定。

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