并行内存障碍

时间:2017-01-14 19:03:53

标签: c# multithreading memory-barriers parallel.for

Microsoft's documention of Parallel.For包含以下方法:

http://www.code-stuff.net
http://www.code-stuff.net/HttpUtility
http://www.code-stuff.net/HttpUtility/minifyCSS
http://www.code-stuff.net/HttpUtility/stringify_json

在此方法中,可能有多个线程从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中,没有显式锁定数组的读写。因为这个例子来自微软,我认为它是线程安全的,但我试图了解幕后发生的事情,以使其成为线程安全的。

就我所理解的情况以及我在SO上提出的其他问题(例如this one)而言,我需要几个记忆障碍才能使这一切顺利进行。那些是:

  1. 创建和初始化Parallel.FormatA后,调用线程上的内存障碍,
  2. 在读取matBmatA的值之前,每个非调用线程上都有内存屏障,
  3. 在将值写入matB
  4. 之后,每个非调用线程上的内存屏障
  5. 在从result读取值之前调用线程上的内存屏障。
  6. 我是否理解正确?

    如果是的话,result会以某种方式完成所有这些吗?我在参考源中挖掘但是在the code之后遇到了麻烦。我没有看到任何Parallel.For阻止或lock来电。

4 个答案:

答案 0 :(得分:6)

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

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

答案 1 :(得分:6)

您正在寻找的内存障碍位于任务计划程序中。

ParallelFor将工作分解为任务,并且工作窃取调度程序执行这些任务。工作窃取调度程序所需的最小内存障碍是:

  1. 创建任务后的“释放”围栏。
  2. 任务被盗时“获取”围栏。
  3. 被盗任务完成时的“释放”围栏。
  4. 正在等待任务的线程的“获取”栅栏。
  5. 查看here,了解用于排队任务的原子(“Interlocked”)操作所暗示的位置1。查看here当任务被盗时,原子操作,易失性读取和/或锁定隐含2。

    我无法追踪3和4的位置。某种原子连接计数器可能暗示了3和4。

答案 2 :(得分:2)

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

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

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

为您编号项目,
  1)和4)由Parallel.For()所暗示   根本不需要2)和3)。

答案 3 :(得分:2)

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

  1. 启动3个数组并使用线程中的值填充。因此,就像为变量赋值并调用方法一样--CLR确保您的方法获得参数的新值。如果初始化是在后台完成的,那么可能可能会占用一席之地,而某些其他线程则同时执行。在这种情况下,你是对的,你需要一些同步结构,内存屏障或lock语句或其他技术。

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

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

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