タスクの順序付けは、タスクの入力を宣言することで指定されます。実行の正確性には、正しい入力宣言が必要です。たとえば、次の2つのタスクには順序付けが指定されていません。
write := IO.write(file("/tmp/sample.txt"), "Some content.")
read := IO.read(file("/tmp/sample.txt"))
sbtは、最初にwrite
を実行してからread
を実行したり、最初にread
を実行してからwrite
を実行したり、read
とwrite
を同時に実行したりすることができます。これらのタスクはファイルを共有しているため、実行は非決定的です。タスクの正しい宣言は次のようになります。
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より前は、このプロセスに対するユーザー制御は以下に制限されていました。
(設定として公開されたことはありませんが、特定の時間に実行されるタスクの最大数も内部的に構成可能でした。)
上記で説明した2番目の構成メカニズムは、プロジェクトのすべてのテストを同じタスクで実行するか、別々のタスクで実行するかを選択するだけでした。各プロジェクトには、テストを実行するための個別のタスクがあり、全体的な実行が並列であれば、別々のプロジェクトのテストタスクを並列で実行できました。すべてのプロジェクトから1つのテストのみを実行するように実行を制限する方法はありませんでした。
sbt 0.12.0では、通常の順序付け宣言を超えてタスクの並行性を制限するための一般的なインフラストラクチャが導入されました。これらの制限には2つの部分があります。
したがって、このシステムはタスクの適切なタグ付けと、適切なルールのセットに依存しています。
一般に、タグには、タグで表されるリソースのタスクの相対的な使用率を表す重みが関連付けられています。現在、この重みは整数ですが、将来的には浮動小数点になる可能性があります。Initialize[Task[T]]
は、構築されたタスクをタグ付けするための2つのメソッドtag
とtagw
を定義します。最初のメソッドtag
は、引数として指定されたタグの重みを1に固定します。2番目のメソッドtagw
は、タグと重みのペアを受け入れます。たとえば、次の例では、CPU
タグとCompile
タグを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 )
)
この例では、以下の制限が設定されています。
これらの制限は、タスクの適切なタグ付けに依存していることに注意してください。また、制限として指定された値は、すべてのタスクが実行できるように1以上である必要があります。この条件が満たされない場合、sbtはエラーを生成します。
ほとんどのタスクは非常に短命であるため、タグ付けされません。これらのタスクには、Untagged
というラベルが自動的に割り当てられます。limitSum
メソッドを使用して、これらのタスクをCPUルールに含めることができます。例えば
...
Tags.limitSum(2, Tags.CPU, Tags.Untagged)
...
制限は最初の引数であるため、タグは可変長引数として指定できることに注意してください。
もう1つの便利な関数は、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
}
...
カスタム関数が従う必要があるいくつかの基本的なルールがありますが、実際に注意すべき主なルールは、タスクが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
メソッドに String を渡します。例えば
val Custom = Tags.Tag("custom")
次に、このタグを他のタグと同様に使用します。例えば
def aImpl = Def.task { ... } tag(Custom)
aCustomTask := aImpl.value
Global / concurrentRestrictions +=
Tags.limit(Custom, 1)
これは実験的な機能であり、変更が必要であったり、さらに作業が必要な点がいくつかあります。
現在、タグはそれが定義されている直近の計算にのみ適用されます。例えば、次の例では、2 番目のコンパイル定義にはタグが適用されていません。最初の計算のみがラベル付けされます。
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 ドキュメントを参照してください。