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
}
在此方法中,可能有多个线程从 matA
和 matB
中读取值,它们都是在调用线程上创建和初始化的,并且可能有多个线程将值写入 result
,稍后由调用线程读取。在传递给 Parallel.For
的 lambda 中,数组读写没有显式锁定。因为此示例来自 Microsoft,所以我假设它是线程安全的,但我试图了解幕后发生的事情以使其成为线程安全的。
根据我阅读的内容和我在 SO 上提出的其他问题(例如 )的理解,需要几个内存障碍才能使这一切正常进行。它们是:
- 创建和初始化
matA
和 matB
、 后调用线程上的内存屏障
- 每个非调用线程在从
matA
和 matB
、 读取值之前的内存屏障
- 在将值写入
result
和 后每个非调用线程上的内存屏障
- 在从
result
. 读取值之前调用线程上的内存屏障
我的理解正确吗?
如果是这样,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) 根本不需要。
我认为您对内存障碍的想法印象深刻,但我真的无法理解您的担忧。让我们看看您调查过的代码:
3 个数组在 main 线程中启动并填充值。所以这就像您为一个变量赋值并调用一个方法——CLR 确保您的方法为参数获取新值。如果初始化由其他线程在后台 and/concurrently 完成,则此处可能出现的问题 可能 会出现。在这种情况下你是对的,你需要一些同步结构,内存屏障或 lock
语句或其他技术。
并行执行的代码获取从 0
到 matARows
的所有值并为它们创建一个任务数组。您需要了解并行化代码的两种不同方式:通过操作和通过数据。在这里,我们有多个行,它们具有相同的 lambda 操作。 temp
变量的赋值不是共享的,所以它们是 thread-safe 并且不需要内存屏障,因为没有旧值和新值。同样,与首先一样,如果其他线程 更新 初始矩阵,则需要在此处进行同步构造。
Parallel.For
确保所有任务都完成(运行 完成,被取消或出错)直到它继续执行下一个语句,因此循环内的代码将是作为普通方法执行。为什么这里不需要屏障?因为所有的写操作都是在不同的行上进行的,它们之间没有交集,所以是一种数据并行。但是,与其他情况一样,如果其他线程需要从某些循环迭代中获取新值,您仍然需要同步。所以这段代码是线程安全的,因为它在数据上是几何并行的,并且不会产生竞争条件。
这个例子很简单,真正的算法一般需要比较复杂的逻辑。您可以研究各种方法来证明代码是线程安全的,而无需使用代码同步 lock-free.
您要查找的内存屏障在任务调度程序中。
ParallelFor 将工作分解为任务,然后 work-stealing 调度程序执行这些任务。 work-stealing 调度程序所需的最小内存屏障是:
- 创建任务后 "release" 围栏。
- 当任务被盗时 "acquire" 围栏。
- 一个 "release" 被盗任务完成时的围栏。
- 等待任务的线程的 "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 可能是由某种原子连接计数器暗示的。
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
}
在此方法中,可能有多个线程从 matA
和 matB
中读取值,它们都是在调用线程上创建和初始化的,并且可能有多个线程将值写入 result
,稍后由调用线程读取。在传递给 Parallel.For
的 lambda 中,数组读写没有显式锁定。因为此示例来自 Microsoft,所以我假设它是线程安全的,但我试图了解幕后发生的事情以使其成为线程安全的。
根据我阅读的内容和我在 SO 上提出的其他问题(例如
- 创建和初始化
matA
和matB
、 后调用线程上的内存屏障
- 每个非调用线程在从
matA
和matB
、 读取值之前的内存屏障
- 在将值写入
result
和 后每个非调用线程上的内存屏障
- 在从
result
. 读取值之前调用线程上的内存屏障
我的理解正确吗?
如果是这样,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) 根本不需要。
我认为您对内存障碍的想法印象深刻,但我真的无法理解您的担忧。让我们看看您调查过的代码:
3 个数组在 main 线程中启动并填充值。所以这就像您为一个变量赋值并调用一个方法——CLR 确保您的方法为参数获取新值。如果初始化由其他线程在后台 and/concurrently 完成,则此处可能出现的问题 可能 会出现。在这种情况下你是对的,你需要一些同步结构,内存屏障或
lock
语句或其他技术。并行执行的代码获取从
0
到matARows
的所有值并为它们创建一个任务数组。您需要了解并行化代码的两种不同方式:通过操作和通过数据。在这里,我们有多个行,它们具有相同的 lambda 操作。temp
变量的赋值不是共享的,所以它们是 thread-safe 并且不需要内存屏障,因为没有旧值和新值。同样,与首先一样,如果其他线程 更新 初始矩阵,则需要在此处进行同步构造。Parallel.For
确保所有任务都完成(运行 完成,被取消或出错)直到它继续执行下一个语句,因此循环内的代码将是作为普通方法执行。为什么这里不需要屏障?因为所有的写操作都是在不同的行上进行的,它们之间没有交集,所以是一种数据并行。但是,与其他情况一样,如果其他线程需要从某些循环迭代中获取新值,您仍然需要同步。所以这段代码是线程安全的,因为它在数据上是几何并行的,并且不会产生竞争条件。
这个例子很简单,真正的算法一般需要比较复杂的逻辑。您可以研究各种方法来证明代码是线程安全的,而无需使用代码同步 lock-free.
您要查找的内存屏障在任务调度程序中。
ParallelFor 将工作分解为任务,然后 work-stealing 调度程序执行这些任务。 work-stealing 调度程序所需的最小内存屏障是:
- 创建任务后 "release" 围栏。
- 当任务被盗时 "acquire" 围栏。
- 一个 "release" 被盗任务完成时的围栏。
- 等待任务的线程的 "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 可能是由某种原子连接计数器暗示的。