1. 并行执行

并行执行 

任务排序 

任务排序是通过声明任务的输入来指定的。执行的正确性需要正确的输入声明。例如,以下两个任务没有指定排序

write := IO.write(file("/tmp/sample.txt"), "Some content.")

read := IO.read(file("/tmp/sample.txt"))

sbt 可以先执行 write,然后执行 read,也可以先执行 read,然后执行 write,或者同时执行 readwrite。这些任务的执行是非确定性的,因为它们共享一个文件。任务的正确声明应该是

write := {
  val f = file("/tmp/sample.txt")
  IO.write(f, "Some content.")
  f
}

read := IO.read(write.value)

这建立了顺序:read 必须在 write 之后运行。我们还保证了 read 将从 write 创建的同一个文件中读取。

实际约束 

注意:本节中描述的功能是实验性的。该功能的默认配置可能会发生变化。

背景 

声明任务的输入和依赖关系可以确保任务被正确排序,并且代码能够正确执行。在实践中,任务共享有限的硬件和软件资源,并且可能需要控制对这些资源的使用。默认情况下,sbt 会并行执行任务(受已描述的排序约束的约束),以尝试利用所有可用的处理器。默认情况下,每个测试类都映射到其自己的任务,以实现并行执行测试。

在 sbt 0.12 之前,用户对该过程的控制仅限于

  1. 启用或禁用所有并行执行(例如,parallelExecution := false)。
  2. 启用或禁用将测试映射到其自己的任务(例如,Test / parallelExecution := false)。

(尽管从未公开为设置,但同时运行的任务的最大数量也在内部可配置。)

上面描述的第二个配置机制仅在运行同一个任务中所有项目的测试或在单独的任务中进行选择。每个项目仍然有一个单独的任务来运行其测试,因此如果整体执行是并行的,则不同项目中的测试任务仍然可以并行运行。无法限制执行,以使所有项目中只有一个测试执行。

配置 

sbt 0.12.0 引入了一个通用基础结构,用于限制任务并发性,超出通常的排序声明。这些限制有两个部分。

  1. 为了对任务的目的和资源利用进行分类,对任务进行标记。例如,编译任务可以标记为 Tags.Compile 和 Tags.CPU。
  2. 一组规则限制了可以同时执行的任务。例如,Tags.limit(Tags.CPU, 4) 将允许最多四个计算密集型任务同时运行。

因此,该系统依赖于对任务进行适当的标记,然后依赖于一组良好的规则。

标记任务 

通常,一个标签与一个权重相关联,该权重代表任务对标签表示的资源的相对利用。目前,此权重是一个整数,但将来可能会成为浮点数。Initialize[Task[T]] 定义了两种用于标记构建的任务的方法:tagtagw。第一个方法 tag 将为传递给它的标签作为参数的权重固定为 1。第二个方法 tagw 接受标签和权重的对。例如,以下示例将 CPUCompile 标签与 compile 任务关联(权重为 1)。

def myCompileTask = Def.task { ... } tag(Tags.CPU, Tags.Compile)

compile := myCompileTask.value

可以通过将标签/权重对传递给 tagw 来指定不同的权重

def downloadImpl = Def.task { ... } tagw(Tags.Network -> 3)

download := downloadImpl.value

定义限制 

一旦任务被标记,concurrentRestrictions 设置就会根据这些任务的加权标签,对可以同时执行的任务进行限制。这必然是一组全局规则,因此它必须在 Global / 范围内。例如,

Global / concurrentRestrictions := Seq(
  Tags.limit(Tags.CPU, 2),
  Tags.limit(Tags.Network, 10),
  Tags.limit(Tags.Test, 1),
  Tags.limitAll( 15 )
)

此示例限制

  • 使用 CPU 的任务数量不超过 2 个
  • 使用网络的任务数量不超过 10 个
  • 测试执行一次仅执行一个测试,涵盖所有项目
  • 任务总数不超过 15 个

请注意,这些限制依赖于对任务进行适当的标记。此外,作为限制提供的必须至少为 1,以确保每个任务都能够执行。如果未满足此条件,sbt 会生成错误。

大多数任务不会被标记,因为它们的生命周期非常短。这些任务会自动分配标签 Untagged。您可以通过使用 limitSum 方法将这些任务包含在 CPU 规则中。例如

...
Tags.limitSum(2, Tags.CPU, Tags.Untagged)
...

请注意,限制是第一个参数,以便标签可以作为可变参数提供。

另一个有用的便利函数是 Tags.exclusive。这指定具有给定标签的任务应该独立执行。它仅在没有其他任务正在运行时开始执行(即使它们具有排他性标签),并且在它完成之前,其他任务也不得开始执行。例如,一个任务可以标记为自定义标签 Benchmark,并配置一个规则以确保此类任务自行执行

...
Tags.exclusive(Benchmark)
...

最后,为了获得最大的灵活性,您可以指定一个类型为 Map[Tag,Int] => Boolean 的自定义函数。Map[Tag,Int] 代表一组任务的加权标签。如果函数返回 true,则表示允许这组任务同时执行。如果返回值为 false,则表示不允许这组任务同时执行。例如,Tags.exclusive(Benchmark) 等同于以下代码

...
Tags.customLimit { (tags: Map[Tag,Int]) =>
  val exclusive = tags.getOrElse(Benchmark, 0)
   //  the total number of tasks in the group
  val all = tags.getOrElse(Tags.All, 0)
   // if there are no exclusive tasks in this group, this rule adds no restrictions
  exclusive == 0 ||
    // If there is only one task, allow it to execute.
    all == 1
}
...

自定义函数必须遵循一些基本规则,但实践中需要注意的主要规则是,如果只有一个任务,则必须允许它执行。如果用户定义的限制完全阻止任务执行,sbt 会生成警告,然后无论如何都会执行该任务。

内置标签和规则 

内置标签在 Tags 对象中定义。下面列出的所有标签都必须由该对象限定。例如,CPU 指的是 Tags.CPU 值。

内置语义标签是

  • Compile - 描述一个编译源代码的任务。
  • Test - 描述一个执行测试的任务。
  • Publish
  • Update
  • Untagged - 当任务没有显式定义任何标签时自动添加。
  • All- 自动添加到每个任务。

内置资源标签是

  • Network - 描述任务的网络利用率。
  • Disk - 描述任务的文件系统利用率。
  • CPU - 描述任务的计算利用率。

当前默认标记的任务是

  • compile : Compile, CPU
  • test : Test
  • update : Update, Network
  • publish, publishLocal : Publish, Network

另外需要注意的是,默认的 test 任务会将其标签传播到为每个测试类创建的每个子任务。

默认规则提供了与先前版本的 sbt 相同的行为

Global / concurrentRestrictions := {
  val max = Runtime.getRuntime.availableProcessors
  Tags.limitAll(if(parallelExecution.value) max else 1) :: Nil
}

如前所述,Test / parallelExecution 控制是否将测试映射到单独的任务。要限制所有项目中并发执行测试的数量,请使用

Global / concurrentRestrictions += Tags.limit(Tags.Test, 1)

自定义标签 

要定义一个新标签,请将字符串传递给Tags.Tag方法。例如

val Custom = Tags.Tag("custom")

然后,将此标签用作任何其他标签。例如

def aImpl = Def.task { ... } tag(Custom)

aCustomTask := aImpl.value

Global / concurrentRestrictions +=
  Tags.limit(Custom, 1)

未来工作 

这是一个实验性功能,有几个方面可能会更改或需要进一步的工作。

标记任务 

目前,标签仅适用于其定义的直接计算。例如,在以下内容中,第二个编译定义没有任何标签应用于它。只有第一个计算被标记。

def myCompileTask = Def.task { ... } tag(Tags.CPU, Tags.Compile)

compile := myCompileTask.value

compile := {
  val result = compile.value
  ... do some post processing ...
}

这是期望的吗?如果没有,更好的替代行为是什么?

分数加权 

权重目前是int,但如果分数权重有用,则可以更改为double。重要的是要保持对权重为 1 的一致概念,以便内置和自定义任务共享此定义,并且可以编写有用的规则。

默认行为 

用户关于哪些自定义规则适用于哪些工作负载的反馈将有助于确定一套良好的默认标签和规则。

对默认值的调整 

规则应该更容易删除或重新定义,也许可以通过给它们命名。就目前而言,规则必须追加或所有规则必须完全重新定义。此外,在使用:=语法时,标签只能为原始定义位置的任务定义。

为了删除标签,removeTag的实现应该以一种直接的方式从tag的实现中得出。

其他特征 

具有权重的标签系统被选择为足够强大且灵活,但又不至于过于复杂。此选择不是根本性的,可以在必要时进行增强、简化或替换。描述系统必须在其内工作的约束的基本接口是sbt.ConcurrentRestrictions。此接口用于在任务执行(sbt.Execute)和底层基于线程的并行执行服务(java.util.concurrent.CompletionService)之间提供一个中间调度队列。此中间队列根据sbt.ConcurrentRestrictions实现限制将新任务转发到j.u.c.CompletionService。有关详细信息,请参阅sbt.ConcurrentRestrictions API 文档。