目录
概念
函数原型
工作原理
与普通 for 循环对比
注意线程安全
概念
ParallelFor
函数的主要目的是将 for
循环操作并行化,以便充分利用多核处理器的优势,加快处理速度,提升程序的性能。通常用于处理可独立对每个元素进行操作、且相互之间没有严格执行顺序依赖的任务集合,比如对数组中的每个元素进行独立的计算、对一批游戏对象进行相同类型的初始化等操作。
函数原型
inline void ParallelFor(int32 Num, TFunctionRef<void(int32)> Body, EParallelForFlags Flags = EParallelForFlags::None)
ParallelFor
函数是一个模板函数,主要接受以下几个参数:
(1)Num
是一个 int32
类型的参数,它指定了循环需要迭代的次数,确定了整个并行循环任务的规模大小,让函数知道需要把任务拆分成多少个子任务来分配到不同线程进行并行处理,是控制循环范围的关键参数
(2)Body
是一个 TFunctionRef<void(int32)>
类型的参数,本质上它是一个可调用对象的引用,通常可以传入 lambda 表达式或者符合相应函数签名的函数指针等。这个可调用对象接受一个 int32
类型的参数,这个参数一般用于表示当前循环迭代的索引值,在可调用对象内部可以根据这个索引值来操作对应的元素或者执行具体的任务逻辑。例如:
ParallelFor(100, [](int32 Index) {// 这里可以根据 Index 来操作对应的元素,比如对数组元素进行赋值、计算等MyArray[Index] = Index * 2;
});
(3)Flags
是一个 EParallelForFlags
类型的参数,默认值为 EParallelForFlags::None
,它用于提供一些额外的执行标志或者控制选项,来影响 ParallelFor
函数的具体执行行为。例如:
1. EParallelForFlags::BackgroundPriority
将并行任务的优先级设置为后台优先级。适用于那些不紧急的计算任务,可以允许系统将更多资源分配给前台任务,如渲染或用户交互。
2. EParallelForFlags::Unbalanced
指示循环中的任务可能不平衡,即某些任务可能需要比其他任务更多的时间来完成。当任务的执行时间差异较大时使用,可以帮助调度器更好地分配线程资源,提高整体效率。
3. EParallelForFlags::ForceSingleThread
强制并行循环在单线程上执行,忽略所有并行设置。用于调试或在某些特定情况下需要确保代码按顺序执行时使用。
4. EParallelForFlags::PumpRenderingThread
允许并行循环在执行过程中泵送渲染线程的消息。确保在进行长时间计算时,渲染线程仍然能够处理其消息队列,防止界面卡顿或渲染延迟。
工作原理
ParallelFor
函数内部会根据系统的处理器核心数量以及当前的负载情况等因素,将整个循环任务划分成多个子任务,并将这些子任务分配到不同的线程(通常是线程池中的线程)去并行执行。它会自动处理好线程的调度、任务的分配以及必要的同步等工作,使得开发者可以相对简单地编写并行代码,而无需深入关注底层的多线程管理细节。
例如,假设有一个四核处理器的系统,使用 ParallelFor
对一个较大的数组进行操作时,它可能会将数组大致平均地分成四个部分(具体的划分策略可能更复杂,会综合考虑多种因素),每个部分的元素操作作为一个子任务,分别交给四个核心对应的线程去同时执行,从而实现并行处理,加快整体的执行速度。
与普通 for
循环对比
普通 for
循环是顺序地依次处理每个元素,只能利用单个核心的计算资源,在面对大量元素操作时,执行速度可能会受限。而 ParallelFor
通过并行化能够利用多核优势,可以同时对多个元素操作,能显著提高处理速度,尤其在处理大规模数据或复杂计算任务时效果更明显。 ParallelFor
虽然能带来性能提升,但由于涉及多线程并行执行,需要考虑更多的因素,比如线程安全问题(在 lambda 表达式中访问共享资源时要特别小心),代码逻辑相对复杂一些,编写和调试时需要更谨慎。
通过如下代码来直观的感受 普通 for
与 ParallelFor
循环的差距:
void UThreadSubsystem::InitParallerFor()
{//检查普通for循环的耗时情况FDateTime startDateTime1 = FDateTime::UtcNow();for (size_t i = 0; i < 10; i++){UE_LOG(LogTemp, Warning, TEXT("This is For: %d"), i);FPlatformProcess::Sleep(0.1);}FDateTime endDateTime1 = FDateTime::UtcNow();FTimespan elapsedTimeSpan1 = endDateTime1 - startDateTime1;double elapsedSeconds1 = elapsedTimeSpan1.GetTotalSeconds();UE_LOG(LogTemp, Warning, TEXT("ElapsedSeconds: %f"), elapsedSeconds1);//检查普通ParallelFor循环的耗时情况FDateTime startDateTime2 = FDateTime::UtcNow();ParallelFor(10, [](int32 i) {UE_LOG(LogTemp, Warning, TEXT("This is ParallelFor: %d"), i);FPlatformProcess::Sleep(0.1);}, EParallelForFlags::BackgroundPriority | EParallelForFlags::Unbalanced);FDateTime endDateTime2 = FDateTime::UtcNow();FTimespan elapsedTimeSpan2 = endDateTime2 - startDateTime2;double elapsedSeconds2 = elapsedTimeSpan2.GetTotalSeconds();UE_LOG(LogTemp, Warning, TEXT("ElapsedSeconds_ParallelFor: %f"), elapsedSeconds2);
}
执行结果如下,可以看到 ParallelFor
循环的代码执行时间约为 普通 for
的1/10,但是执行顺序是乱的。
注意线程安全
由于 ParallelFor
是并行执行任务的,当 lambda 表达式中涉及到对共享资源(如全局变量、共享的游戏对象等)的访问时,必须要注意线程安全。如果多个线程同时读写同一个共享资源,没有合适的多线程同步机制保障,很容易导致数据不一致、程序崩溃等问题。
这里通过对变量加锁来确保同一时刻只有一个线程能够对其进行修改,从而保证线程安全,示例代码如下:
可以看到计算结果相同, ParallelFor
循环的代码执行时间依旧远快于普通 for
循环。