【发布时间】:2016-02-11 03:41:07
【问题描述】:
我是使用 TPL 进行并行编程的新手,刚刚听完 TPL 课程。我刚刚写了一小段演示软件来测试我对它的理解。
在问我的一般问题之前,让我解释一下我的并行代码的上下文:(如果无聊,直接跳到问题;-)
上下文:
我首先编写了一个遍历递归代码的顺序递归树(适用于任何已知的两人游戏),然后尝试通过在每个遍历(和评估)节点上创建并发子任务来将其并行化,从而尽可能多地展示并行性. (为每个子节点启动一个子任务)。从当前评估的头节点的每个子节点发出的子树通过启动的并发子任务之一调用的相同递归方法自行评估。
在游戏树中,节点代表游戏位置。当前游戏位置的根节点及其 n 个子节点(在第 2 层树上)通过在根位置为玩家 A 应用 n 次合法移动可以到达的游戏位置。第 3 级节点通过在每个第 2 级位置/节点上应用玩家 B 下的合法移动来表示可到达的位置,依此类推,直到达到给定的最大搜索深度。..
树以深度优先遍历顺序遍历,直到给定的最大搜索深度,其中“叶子”位置将由启发式评估函数评估,该函数返回一个整数值,表示玩家 A 与玩家 B 在此方面的优势游戏位置。这个值越大,玩家 A 的位置就应该越好。
将所有子节点的最佳值报告给它们共同的父节点,(当然如果父节点代表玩家A所打的位置,则最佳子节点是具有最高值的子节点,如果它由玩家 B 播放,最好的子节点是值最低的那个)。
最好的子值最终以向根节点报告其最佳值的第 2 级子节点的索引报告给根节点。当然,这个指数代表玩家 A 的“最佳”移动。这意味着向玩家 A 保证报告值(或更好的值)可以在探索深度(“叶子”位置)达到,无论玩家 B 的走法。
好的,这是众所周知的两人状态博弈的极小极大算法。
您可能还知道这种顺序算法的经典优化,称为 alpha-beta,当我们知道博弈树的一些分支/子分支不会导致有趣的叶子位置时,它们允许修剪它们。
所以我尝试并行化这种递归树遍历,将叶子值报告给根节点,并允许完全取消树遍历(如果用户取消搜索),但也允许取消部分子树任务以实现 alpha-beta修剪必须中止执行某些子节点子树评估的任务,而不会中断在其他子树上工作的任务。
工作成功完成,并行搜索比顺序搜索更快,并且几乎总是以 100% 的速度在所有可用内核上分派工作(根据任务管理器)。我不想用我的源代码让你厌烦。我只想准确地说,我还将 .Net 4.5 异步等待模式应用于我的递归并行化方法,并实现了“一个接一个地等待”模式(实际上是等待 Task.WhenAll())来创建探索子任务从每个子节点发出的子树。 (注意:为了允许子树遍历取消,我必须将取消令牌列表传递给递归异步子树探索方法,每个递归探索级别都会向列表中添加一个新的 CancellationToken)。
现在您已经了解了上下文,下面是我的问题:
启动搜索将快速创建数百万个任务,评估从每个树节点发出的数百万个子树,但实际上只有几个处理器内核可用于使用几个线程(每个内核一个?)执行这些任务。那么不考虑这一点并为每个必须评估/遍历的子节点/子树创建任务是一个好主意吗?创建比暴露最大并行度所需的任务更多的任务是否不会引起开销?限制启动任务的数量不是更好吗(例如,只为最高级别的节点启动任务,而对最低级别的节点使用纯顺序遍历方法?)或者我们可以通过创建数百万个任务,即使我们知道其中一些(在最深的树节点上启动)执行的工作非常小?
我没有阻止“通过闭包将循环索引变量传递给启动的任务”问题使用 lambda 表达式参数(如课程中建议的那样),而是通过复制循环索引在启动任务并通过闭包引用此副本之前,本地副本变量中的变量,而不是 lambda 表达式中的索引循环变量。您如何看待这种解决方法?这和传递 lambda 表达式参数一样安全吗?
minimax 算法的 alpha-beta 剪枝优化基于每个子树的顺序遍历。并行遍历使 alpha-beta 修剪效率降低,因为如果子节点的所有并行子树遍历花费大致相同的时间返回,则不会中止任何子树遍历任务,如果它们将被中止,它们将被中止,因为它们都是已经快完成了……这将极大地限制 alpha-beta 优化所带来的收益。在将博弈树遍历与 alpha-beta 优化并行化时,您知道解决此问题的明智策略吗?
【问题讨论】:
-
不,任务不是线程。另一方面,如果要处理大量数据,则应使用 Data (PLINQ),而不是 Task 并行来划分数据并避免调度任务的开销在线程池上。您还应该检查 .NET 4.6(Vector 类)中的 SIMD 支持,因为它提供了至少 4 倍的性能提升,而没有单核的开销。单个 CPU 操作不是一次在单个浮点上工作,而是在 4 个上工作
-
是的,任务不是线程。但是,当然,任务是在线程上执行的,创建大量任务,即使是非常小批量的工作(低级节点),也意味着很多上下文更改可能会导致开销。这就是为什么我想知道是否应该通过顺序方法遍历最深的节点/子树。感谢您提供替代建议(PLINQ 和 SIMD),不确定在我的上下文中是否容易实现,但我会考虑一下。
-
这就是为什么在数据并行中你对数据进行分区然后使用一小组任务来处理它。当您有许多应该同时/异步运行的单独任务时,使用任务并行。但是,在您的情况下,您有很多节点要处理,这使得数据并行是合适的。数据并行算法虽然具有与顺序算法不同的结构(例如在简单循环中除外)。这就像将 FOR 循环更改为 SQL 语句或 MapReduce 作业。
-
那么有多少任务实际上是“活的”(存在)的? (您可以使用原子递增/递减计数器和高水位线来跟踪它)。
-
这取决于您所说的“实时”任务,如果我在创建任务之前增加计数器并在此任务完成后减少它(在等待语句之后)(以及捕获 OperationCanceledException由取消的任务引发,为每个仍然等待的任务减少一次计数器)我同时获得数百万个现有任务。 (围绕遍历树的总节点)。
标签: .net parallel-processing task-parallel-library task alpha-beta-pruning