Parallel procedures/zh CN: what changed
Revision 0 to 1. Removed lines have a minus sign; added lines have a plus sign.
Title: Parallel procedures/zh CN
| Old | New | Line | |
|---|---|---|---|
| 1 | + | # 概述 | |
| 2 | + | ||
| 3 | + | 本页描述如何使用MTProcs单元以并行方式运行单个过程,这简化了并行过程的运行以及并行算法的实现。 | |
| 4 | + | ||
| 5 | + | 并行过程和方法经常出现在并行算法中,一些编程语言为它们提供了内置支持(例如gcc中的OpenMP)。有关向FPC添加此类语言特性的计划,请参阅[[OpenMP支持]]。将这些内容嵌入到语言中可以节省一些键入,并允许编译器生成开销较少的代码。另一方面,有许多方法可以将单线程代码段转换为并行代码。事实上,简单的方法往往会减慢代码速度。为了获得好的结果,必须指定一些编译器无法猜测的参数。有关示例,请参阅[OpenMP](<http://openmp.org/wp/>)和[OpenCL](<http://www.khronos.org/opencl/>)的大量设置和讨论。你需要并行算法。MTProcs有助于实现并行算法。 | |
| 6 | + | ||
| 7 | + | # 获取MTProcs | |
| 8 | + | ||
| 9 | + | 单元**mtprocs.pas** 是**multithreadprocslaz.lpk** 包的一部分,该包不需要其他包,只需FPC >= 2.6.0。 | |
| 10 | + | ||
| 11 | + | 你可以在sourceforge上找到其源代码: | |
| 12 | + | ``` | |
| 13 | + | svn co https://lazarus-ccr.svn.sourceforge.net/svnroot/lazarus-ccr/components/multithreadprocs multithreadprocs | |
| 14 | + | ``` | |
| 15 | + | ||
| 16 | + | 或者在Lazarus的 _components/multithreadprocs_ 中。 | |
| 17 | + | ||
| 18 | + | 一如既往:在IDE中打开一次multithreadprocslaz.lpk包,以便它学习路径。 要在你的项目中使用该包:使用 _IDE Menu / Package / Open recent package / .../multithreadprocslaz.lpk / More / add to project_ 将其添加到当前项目中。或者通过项目检查器添加。 | |
| 19 | + | ||
| 20 | + | # 简单示例 | |
| 21 | + | ||
| 22 | + | 以下是一个简短的示例,它不做任何有用的事情,但演示了并行过程的样子以及如何调用它: | |
| 23 | + | ``` | |
| 24 | + | program Test; | |
| 25 | + | ||
| 26 | + | {$mode objfpc}{$H+} | |
| 27 | + | ||
| 28 | + | uses | |
| 29 | + | {$IFDEF UNIX} | |
| 30 | + | cthreads, cmem, | |
| 31 | + | {$ENDIF} | |
| 32 | + | MTProcs; | |
| 33 | + | ||
| 34 | + | // 一个简单的并行过程 | |
| 35 | + | procedure DoSomethingParallel(Index: PtrInt; Data: Pointer; Item: TMultiThreadProcItem); | |
| 36 | + | var | |
| 37 | + | i: Integer; | |
| 38 | + | begin | |
| 39 | + | writeln(Index); | |
| 40 | + | for i:=1 to Index*1000000 do ; // 执行一些工作 | |
| 41 | + | end; | |
| 42 | + | ||
| 43 | + | begin | |
| 44 | + | ProcThreadPool.DoParallel(@DoSomethingParallel,1,5,nil); // 方法地址,起始索引,结束索引,可选数据 | |
| 45 | + | end. | |
| 46 | + | ``` | |
| 47 | + | ||
| 48 | + | 输出将是类似这样的: | |
| 49 | + | ``` | |
| 50 | + | 2 | |
| 51 | + | 3 | |
| 52 | + | 1 | |
| 53 | + | 4 | |
| 54 | + | 5 | |
| 55 | + | ``` | |
| 56 | + | ||
| 57 | + | 以下是一些简短的说明。详细信息稍后提供。 | |
| 58 | + | ||
| 59 | + | * 在Unix下,多线程需要单元**cthreads** ,如[[Multithreaded Application Tutorial]]中所述。 | |
| 60 | + | * 出于速度考虑,建议使用**cmem** 堆管理器,尽管在此示例中没有任何差异。 | |
| 61 | + | * 并行过程**DoSomethingParallel** 获取一些固定和预定义的参数。 | |
| 62 | + | * **Index** 定义了应由此次调用完成的工作块。 | |
| 63 | + | * **Data** 是指针,作为第四个参数传递给 _ProcThreadPool.DoParallel_ 。它是可选的,你可以自由地使用它来做任何事情。 | |
| 64 | + | * **Item** 可用于访问线程池的一些更复杂的功能。 | |
| 65 | + | * **ProcThreadPool.DoParallel** 的工作就像一个正常的过程调用。当它完全运行时返回 - 这意味着所有线程都已完成其工作。 | |
| 66 | + | * 输出显示了典型的多线程行为:调用的顺序是不确定的。几次运行可能会导致不同的顺序。 | |
| 67 | + | ||
| 68 | + | # 特性 | |
| 69 | + | ||
| 70 | + | 并行运行过程意味着: | |
| 71 | + | ||
| 72 | + | * 过程或方法以从任意StartIndex到任意EndIndex的Index执行。 | |
| 73 | + | * 一个或多个线程并行执行这些索引。例如,如果Index从1运行到10,并且池中有3个线程可用,那么3个线程将同时运行三个不同的索引。每次线程完成一个调用(一个索引)时,它都会分配下一个索引并运行它。结果可能是:线程1执行索引3,5,7,线程2执行1,6,8,9,线程3运行2,4,10。 | |
| 74 | + | * 线程的数量可能在运行期间变化,并且没有最小线程数的保证。在最坏的情况下,所有索引都将由一个线程执行 - 线程本身。 | |
| 75 | + | * 最大线程数以对当前系统的良好猜测初始化。它可以通过 | |
| 76 | + | ||
| 77 | + | ``` | |
| 78 | + | ProcThreadPool.MaxThreadCount := 8; | |
| 79 | + | ``` | |
| 80 | + | ||
| 81 | + | 随时手动设置。 | |
| 82 | + | ||
| 83 | + | * 你可以为每个过程设置最大线程数。 | |
| 84 | + | * 并行过程(或方法)可以递归地调用并行过程(或方法)。 | |
| 85 | + | * 线程是重用的,这意味着它们不是为每个索引创建和销毁的,而是有一个全局线程池。在双核处理器上,将有两个线程完成所有工作 - 主线程和池中的一个额外线程。 | |
| 86 | + | ||
| 87 | + | # 开销,减速 | |
| 88 | + | ||
| 89 | + | 开销在很大程度上取决于系统(核心的数量和类型,共享内存的类型,关键部分的速度,缓存大小)。以下是一些一般提示: | |
| 90 | + | ||
| 91 | + | * 每个工作块(索引)应该至少占用几毫秒。 | |
| 92 | + | * 开销与并行过程的递归级别无关。 | |
| 93 | + | ||
| 94 | + | 与MTProcs单元无关,但仅由当今计算机体系结构产生的多线程开销: | |
| 95 | + | ||
| 96 | + | * 一旦创建一个线程,你的程序就变成多线程,内存管理器必须使用关键部分,这会减慢速度。因此,即使你不使用线程做任何事情,你的程序也可能会变慢。 | |
| 97 | + | * 在一些系统上,对于多线程,cmem堆管理器要快得多。在我的基准测试中,特别是在Intel系统上,特别是在OS X下,速度差异可能超过10倍。 | |
| 98 | + | * 字符串和接口是全局引用计数的。每次访问都需要一个关键部分。因此,在多个线程中处理字符串几乎不会提高速度。请使用PChars代替。 | |
| 99 | + | * 每个工作块(索引)应该在内存的不相交部分上工作,以避免跨缓存更新。 | |
| 100 | + | * 不要处理大量内存。在一些系统上,单个线程的速度就足够填满内存总线速度。当达到内存总线的最大速度时,任何进一步的线程都会减慢速度而不是加快速度。 | |
| 101 | + | ||
| 102 | + | # 设置过程的最大线程数 | |
| 103 | + | ||
| 104 | + | 你可以将过程的最大线程数指定为第五个参数。 | |
| 105 | + | ``` | |
| 106 | + | begin | |
| 107 | + | ProcThreadPool.DoParallel(@DoSomethingParallel,1,10,nil,2); // 地址,起始索引,结束索引, | |
| 108 | + | // 可选:数据(此处为nil),可选:最大线程数(此处为2) | |
| 109 | + | end. | |
| 110 | + | ``` | |
| 111 | + | ||
| 112 | + | 当线程在同一数据上工作,并且太多线程会创建太多缓存冲突,从而相互减慢速度时,这可能会很有用。或者,当算法大量使用WaitForIndex时,以便只有少数线程可以实际工作。然后,线程可以用于其他任务。 | |
| 113 | + | ||
| 114 | + | # 等待索引/按顺序执行 | |
| 115 | + | ||
| 116 | + | 有时,一个Index依赖于前一个Index的结果。例如,一个常见的任务是首先计算块5,然后将结果与块3的结果组合。为此,请使用WaitForIndex方法: | |
| 117 | + | ``` | |
| 118 | + | procedure DoSomethingParallel(Index: PtrInt; Data: Pointer; Item: TMultiThreadProcItem); | |
| 119 | + | begin | |
| 120 | + | ... 计算块号'Index' ... | |
| 121 | + | if Index=5 then begin | |
| 122 | + | if not Item.WaitForIndex(3) then exit; | |
| 123 | + | ... 计算 ... | |
| 124 | + | end; | |
| 125 | + | end; | |
| 126 | + | ``` | |
| 127 | + | ||
| 128 | + | WaitForIndex接受一个低于当前Index的Index或一个范围作为参数。如果它返回true,则一切按预期工作。如果它返回false,则表示其他线程之一发生了异常。 | |
| 129 | + | ||
| 130 | + | 有一个扩展函数WaitForIndexRange,它等待整个Index范围: | |
| 131 | + | ``` | |
| 132 | + | if not Item.WaitForIndexRange(3,5) then exit; // 等待3,4和5 | |
| 133 | + | ``` | |
| 134 | + | ||
| 135 | + | # 异常 | |
| 136 | + | ||
| 137 | + | 如果其中一个线程中发生异常,其他线程将正常完成,但不会开始新的Index。池等待所有线程完成,然后引发异常。这就是为什么你可以像往常一样使用 _try..except_ : | |
| 138 | + | ``` | |
| 139 | + | try | |
| 140 | + | ... | |
| 141 | + | ProcThreadPool.DoParallel(...); | |
| 142 | + | ... | |
| 143 | + | except | |
| 144 | + | On E: Exception do ... | |
| 145 | + | end; | |
| 146 | + | ``` | |
| 147 | + | ||
| 148 | + | 如果有多个异常,则只会引发第一个异常。要处理所有异常,请在并行方法中添加try..except。 | |
| 149 | + | ||
| 150 | + | # 同步 | |
| 151 | + | ||
| 152 | + | 当你想在主线程中调用一个函数时,例如更新一些gui元素,你可以使用类方法 _TThread.Synchronize_ 。它接受当前TThread和一个方法地址作为参数。自1.2版起,mtprocs提供了一个线程变量**CurrentThread** ,它简化了同步: | |
| 153 | + | ``` | |
| 154 | + | TThread.Synchronize(CurrentThread,@YourMethod); | |
| 155 | + | ``` | |
| 156 | + | ||
| 157 | + | 这将在主事件队列上发布一个事件,并等待直到主线程执行了你的方法。请记住,一个裸露的fpc程序没有事件队列。LCL或fpgui程序有。 | |
| 158 | + | ||
| 159 | + | 如果你创建了自己的TThread后代,你应该在你的Execute方法中设置这个变量。例如: | |
| 160 | + | ``` | |
| 161 | + | procedure TYourThread.Execute; | |
| 162 | + | begin | |
| 163 | + | CurrentThread:=Self; // 设置当前线程为自身 | |
| 164 | + | ...工作... // 执行相关工作 | |
| 165 | + | end; | |
| 166 | + | ``` | |
| 167 | + | ||
| 168 | + | # 示例:并行循环 | |
| 169 | + | ||
| 170 | + | 本示例逐步解释如何将循环转换为并行过程。该示例计算整数数组“BigArray”的最大值。 | |
| 171 | + | ||
| 172 | + | ## 原始循环 | |
| 173 | + | ``` | |
| 174 | + | type | |
| 175 | + | TArrayOfInteger = array of integer; | |
| 176 | + | ||
| 177 | + | function FindMaximum(BigArray: TArrayOfInteger): integer; | |
| 178 | + | var | |
| 179 | + | i: PtrInt; | |
| 180 | + | begin | |
| 181 | + | Result:=BigArray[0]; | |
| 182 | + | for i:=1 to length(BigArray)-1 do begin | |
| 183 | + | if Result<BigArray[i] then Result:=BigArray[i]; | |
| 184 | + | end; | |
| 185 | + | end; | |
| 186 | + | ``` | |
| 187 | + | ||
| 188 | + | ## 分配工作 | |
| 189 | + | ||
| 190 | + | 工作应平均分配给n个线程。为此,将BigArray分割成大小相等的块,并且外层循环遍历每个块。通常,n是系统中的CPU/核心数。MTProcs有一些实用函数用于计算块大小和数量: | |
| 191 | + | ``` | |
| 192 | + | function FindMaximum(BigArray: TArrayOfInteger): integer; | |
| 193 | + | var | |
| 194 | + | BlockCount, BlockSize: PtrInt; | |
| 195 | + | i: PtrInt; | |
| 196 | + | Index: PtrInt; | |
| 197 | + | BlockStart, BlockEnd: PtrInt; | |
| 198 | + | begin | |
| 199 | + | Result:=BigArray[0]; | |
| 200 | + | ProcThreadPool.CalcBlockSize(length(BigArray),BlockCount,BlockSize); // 计算块大小和数量 | |
| 201 | + | for Index:=0 to BlockCount-1 do begin | |
| 202 | + | Item.CalcBlock(Index,BlockSize,length(BigArray),BlockStart,BlockEnd); // 计算每个块的起始和结束索引 | |
| 203 | + | for i:=BlockStart to BlockEnd do begin | |
| 204 | + | if Result<BigArray[i] then Result:=BigArray[i]; | |
| 205 | + | end; | |
| 206 | + | end; | |
| 207 | + | end; | |
| 208 | + | ``` | |
| 209 | + | ||
| 210 | + | 添加的几行代码可用于任何循环。最终可以编写一个工具来自动化此过程。 | |
| 211 | + | ||
| 212 | + | 现在工作已被分割成较小的部分。接下来,这些部分必须变得更加独立。 | |
| 213 | + | ||
| 214 | + | ## 局部变量和共享变量 | |
| 215 | + | ||
| 216 | + | 对于循环中使用的每个变量,必须确定它是由所有线程使用的共享变量,还是每个线程使用其自己的局部变量。 共享变量BlockCount和BlockSize仅被读取且不会更改,因此无需对它们进行处理。 但是,像Result这样的共享变量将被所有线程更改。这可以通过同步(例如临界区)来实现,但速度较慢,或者每个线程使用局部副本,然后合并这些局部变量。 | |
| 217 | + | ||
| 218 | + | 以下是一个解决方案,将“Result”变量替换为数组,最后进行合并: | |
| 219 | + | ``` | |
| 220 | + | function FindMaximum(BigArray: TArrayOfInteger): integer; | |
| 221 | + | var | |
| 222 | + | // 共享变量 | |
| 223 | + | BlockCount, BlockSize: PtrInt; | |
| 224 | + | BlockMax: PPtrInt; | |
| 225 | + | // 局部变量 | |
| 226 | + | i: PtrInt; | |
| 227 | + | Index: PtrInt; | |
| 228 | + | BlockStart, BlockEnd: PtrInt; | |
| 229 | + | begin | |
| 230 | + | ProcThreadPool.CalcBlockSize(length(BigArray),BlockCount,BlockSize); | |
| 231 | + | BlockMax:=AllocMem(BlockCount*SizeOf(PtrInt)); // 为局部变量分配空间 | |
| 232 | + | // 计算每个块的最大值 | |
| 233 | + | for Index:=0 to BlockCount-1 do begin | |
| 234 | + | // 计算块的最大值 | |
| 235 | + | Item.CalcBlock(Index,BlockSize,length(BigArray),BlockStart,BlockEnd); | |
| 236 | + | BlockMax[Index]:=BigArray[BlockStart]; | |
| 237 | + | for i:=BlockStart to BlockEnd do begin | |
| 238 | + | if BlockMax[Index]<BigArray[i] then BlockMax[Index]:=BigArray[i]; | |
| 239 | + | end; | |
| 240 | + | end; | |
| 241 | + | // 计算所有块的最大值 | |
| 242 | + | // (如果有数百个线程,则有更好的解决方案) | |
| 243 | + | Result:=BlockMax[0]; | |
| 244 | + | for Index:=1 to BlockCount-1 do | |
| 245 | + | Result:=Max(Result,BlockMax[Index]); | |
| 246 | + | ||
| 247 | + | FreeMem(BlockMax); | |
| 248 | + | end; | |
| 249 | + | ``` | |
| 250 | + | ||
| 251 | + | 这种方法简单明了,可以自动化。但是,此过程将需要程序员提供一些提示。 | |
| 252 | + | ||
| 253 | + | ## DoParallel | |
| 254 | + | ||
| 255 | + | 最后一步是将内部循环移入子过程,并用对DoParallelNested的调用替换循环。 | |
| 256 | + | ``` | |
| 257 | + | ... | |
| 258 | + | {$ModeSwitch nestedprocvars} | |
| 259 | + | uses mtprocs; | |
| 260 | + | ... | |
| 261 | + | function TMainForm.FindMaximum(BigArray: TArrayOfInteger): integer; | |
| 262 | + | var | |
| 263 | + | BlockCount, BlockSize: PtrInt; | |
| 264 | + | BlockMax: PPtrInt; | |
| 265 | + | ||
| 266 | + | procedure FindMaximumParallel(Index: PtrInt; Data: Pointer; | |
| 267 | + | Item: TMultiThreadProcItem); | |
| 268 | + | var | |
| 269 | + | i: integer; | |
| 270 | + | BlockStart, BlockEnd: PtrInt; | |
| 271 | + | begin | |
| 272 | + | // 计算块的最大值 | |
| 273 | + | Item.CalcBlock(Index,BlockSize,length(BigArray),BlockStart,BlockEnd); | |
| 274 | + | BlockMax[Index]:=BigArray[BlockStart]; | |
| 275 | + | for i:=BlockStart to BlockEnd do | |
| 276 | + | if BlockMax[Index]<BigArray[i] then BlockMax[Index]:=BigArray[i]; | |
| 277 | + | end; | |
| 278 | + | var | |
| 279 | + | Index: PtrInt; | |
| 280 | + | begin | |
| 281 | + | // 将工作分割成大小相等的块 | |
| 282 | + | ProcThreadPool.CalcBlockSize(length(BigArray),BlockCount,BlockSize); | |
| 283 | + | // 分配局部/线程变量 | |
| 284 | + | BlockMax:=AllocMem(BlockCount*SizeOf(PtrInt)); | |
| 285 | + | // 计算每个块的最大值 | |
| 286 | + | ProcThreadPool.DoParallelNested(@FindMaximumParallel,0,BlockCount-1); | |
| 287 | + | // 计算所有块的最大值 | |
| 288 | + | Result:=BlockMax[0]; | |
| 289 | + | for Index:=1 to BlockCount-1 do | |
| 290 | + | Result:=Max(Result,BlockMax[Index]); | |
| 291 | + | FreeMem(BlockMax); | |
| 292 | + | end; | |
| 293 | + | ``` | |
| 294 | + | ||
| 295 | + | 这大多是复制和粘贴,因此同样可以自动化。 | |
| 296 | + | ||
| 297 | + | # 示例:并行排序 | |
| 298 | + | ||
| 299 | + | 单元mtputils包含函数“ParallelSortFPList”,该函数使用mtprocs对TFPList进行并行排序。必须提供比较函数。 | |
| 300 | + | ``` | |
| 301 | + | procedure ParallelSortFPList(List: TFPList; const Compare: TListSortCompare; MaxThreadCount: integer = 0; const OnSortPart: TSortPartEvent = nil); | |
| 302 | + | ``` | |
| 303 | + | ||
| 304 | + | 此函数使用并行MergeSort算法。参数MaxThreadCount传递给DoParallel。0表示使用系统默认值。 | |
| 305 | + | ||
| 306 | + | 可选地,您可以提供自己的排序函数(OnSortPart)来对每个单线程的部分进行排序。例如,您可以通过QuickSort对块进行排序,然后再进行合并。这样您就拥有了一个“并行QuickSort”。有关QuickSort的示例实现,请参阅TFPList.Sort。 | |
| 307 | + | ||
| 308 | + | # 使用嵌套过程 | |
| 309 | + | ``` | |
| 310 | + | procedure DoSomething(Value: PtrInt); | |
| 311 | + | var | |
| 312 | + | p: array[1..2] of Pointer; | |
| 313 | + | ||
| 314 | + | procedure SubProc(Index: PtrInt; Data: Pointer; Item: TMultiThreadProcItem); | |
| 315 | + | begin | |
| 316 | + | p[Index]:=Pointer(Value); // 可以访问局部变量和参数! | |
| 317 | + | end; | |
| 318 | + | ||
| 319 | + | var | |
| 320 | + | i: Integer; | |
| 321 | + | begin | |
| 322 | + | ProcThreadPool.DoParallelNested(@SubProc,1,2); | |
| 323 | + | end; | |
| 324 | + | ``` | |
| 325 | + | ||
| 326 | + | 这可以节省大量重构工作,并使并行过程更加可读。 | |
| 327 | + | ||
| 328 | + | # See also | |
| 329 | + | ||
| 330 | + | * [[Multithreaded Application Tutorial]] | |
| 331 | + | * [[OpenCL]] | |
| 332 | + | ||
| 333 | + | --- | |
| 334 | + | *From the Free Pascal wiki, page "Parallel procedures/zh CN", as the Internet Archive kept it on 2025-02-18, last changed on 2024-08-14. Copyright stays with the author who wrote it, according to the old wiki's [[Licensing]] page. If you have concerns about this material, [message the admin](/wiki-contact).* |
