编辑:正如@edharned指出的
.parallel()
现在使用
CountedCompleter
而不是打电话
.join()
,其中有自己的问题,Ed在
http://coopsoft.com/ar/Calamity2Article.html
下
What is currently being done?
部分。
我相信下面的信息对于理解fork-join框架的复杂性以及提出的
.parallel()
其中的结论仍然是相关的。
虽然代码的精神是正确的,但实际的代码可以对所有使用
.parallel()
尽管这一点都不明显。
这是我读了一堆书之后的想法:
并行()
在Java中使用
ForkJoinPool.commonPool()
这是一个单子
ForkJoinPool
所有流共享(
forkJoinPool.commonPool()
是公共静态方法,因此理论上其他库/代码部分可以使用它)
FokCounPo水池
实现工作窃取,并且除了共享队列之外还有每线程队列
偷工减料是指当一根线空闲时,它会寻找更多的工作来做
最初我想:根据这个定义,不是
cached
线程池也会进行工作窃取(即使某些引用调用缓存线程池的工作共享)?
结果发现,在使用idle这个词时,似乎有些术语模糊:
在一个
缓存的
线程池,线程只有在完成其任务后才空闲。它确实
不
如果在等待阻塞呼叫时被阻塞,则变为空闲
在一个
forkjoin
线程池,线程在完成其任务或调用
连接()
方法(这是一个特殊的阻塞调用)。
什么时候?
连接()
在子任务上调用时,线程在等待该子任务完成时变为空闲。空闲时,它将尝试执行任何其他可用的任务,即使它在另一个线程的队列中(它会窃取工作)。
[这是最重要的一点]
一旦找到要执行的另一个任务,它必须在恢复其原始执行之前完成它,即使它等待的子任务在线程仍在执行被盗任务时完成。
[这也很重要]
此工作窃取行为仅适用于调用
连接()
. 如果一个线程被其他东西阻塞,比如I/O,它就会变得空闲(即它不会窃取工作)。
我花了一段时间才明白
2.3.2
,因此我将给出一个快速示例来帮助说明该问题:
注意:这些都是伪示例,但是您可以在没有意识到的情况下使用streams进入相同的情况,streams在内部执行fork-join操作。
另外,我将使用极其简化的伪代码,这些代码只用于说明.parallel()问题,但不一定有其他意义。
假设我们正在实现合并排序
merge_sort(list):
left, right = split(list)
leftTask = mergeSortTask(left).fork()
rightTask = mergeSortTaks(right).fork()
return merge(leftTask.join(), rightTask.join())
现在假设我们有另一段代码可以执行以下操作:
dummy_collect_results(queriesIds):
pending_results = []
for id in queriesIds:
pending_results += longBlockingIOTask(id).fork()
// do more stuff
这里发生了什么?
在编写合并排序代码时,您认为排序调用不做任何I/O,因此它们的性能应该是非常确定的,对吧?
正确的。你可能没想到,因为
dummy_collect_results
方法在执行MergeSort任务的线程阻止
连接()
,等待子任务完成,它们可能开始执行长阻塞子任务之一。
这很糟糕,因为如上所述,一旦长阻塞(在I/O上,不是
连接()
调用,使线程不再空闲)已被窃取,必须完成,无论线程是否通过
连接()
在阻塞I/O时完成。
这使得mergesort任务的执行不再具有确定性,因为执行这些任务的线程可能最终窃取由完全位于其他地方的代码生成的i/o密集型任务。
这也很吓人,很难抓住,因为你可能一直在使用
并行()
在整个代码库中没有任何问题,只需要一个类在使用时引入长时间运行的任务
并行()
突然之间,代码库的所有其他部分可能会得到不一致的性能。
所以我的结论是:
理论上,
.parallel()
如果您可以保证在代码中任何地方创建的所有任务都很短,那么就可以了
并行()
可能会对整个系统的性能产生不明显的影响,除非您知道(例如,如果您稍后添加一段使用
.parallel()
而且任务很长,可能会影响所有使用
并行()
)