web-dev-qa-db-ja.com

Juliaで@asyncと@syncをいつどのように使用するか

@asyncマクロと@syncマクロの documentation を読みましたが、それらをいつどのように使用するのかわからず、他の場所でそれらの多くのリソースや例を見つけることができませんインターネット。

私の当面の目標は、複数のワーカーを並行して実行するように設定し、それらがすべて終了するまで待機してコードを続行する方法を見つけることです。この投稿: ジュリアのリモートプロセッサでタスクが完了するのを待つ には、これを成功させるための1つの成功した方法が含まれています。 @asyncマクロと@syncマクロを使用して可能だと思っていましたが、これを達成できなかった最初の失敗により、これらのマクロをいつどのように使用するかを適切に理解しているかどうか疑問に思いました。

23

?@asyncのドキュメントによると、「@asyncは式をタスクにラップします。」つまり、スコープ内にあるものは何でも、ジュリアはこのタスクの実行を開始しますが、タスクが完了するのを待たずに、スクリプトの次の処理に進みます。したがって、たとえば、マクロがなければ次のようになります。

Julia> @time sleep(2)
  2.005766 seconds (13 allocations: 624 bytes)

しかし、マクロを使用すると、次のようになります。

Julia> @time @async sleep(2)
  0.000021 seconds (7 allocations: 657 bytes)
Task (waiting) @0x0000000112a65ba0

Julia> 

したがって、Juliaは、タスク(この場合は2秒間スリープ)が完了するのを待たずに、スクリプトを続行(および@timeマクロを完全に実行)できます。

対照的に、@syncマクロは、「@async@spawn@spawnatおよび@parallelの動的に囲まれた使用がすべて完了するまで待機します。」 (?@syncのドキュメントによると)。したがって、次のようになります。

Julia> @time @sync @async sleep(2)
  2.002899 seconds (47 allocations: 2.986 KB)
Task (done) @0x0000000112bd2e00

この単純な例では、@async@syncの単一のインスタンスを一緒に含めることに意味はありません。しかし、@syncが役立つのは、@asyncを複数の操作に適用して、それぞれが完了するのを待たずに一度にすべてを開始できるようにする場合です。

たとえば、複数のワーカーがいて、それぞれが同時にタスクで作業を開始し、それらのタスクから結果をフェッチしたいとします。最初の(ただし正しくない)試みは次のとおりです。

using Distributed
cell(N) = Vector{Any}(undef, N)

addprocs(2)
@time begin
    a = cell(nworkers())
    for (idx, pid) in enumerate(workers())
        a[idx] = remotecall_fetch(sleep, pid, 2)
    end
end
## 4.011576 seconds (177 allocations: 9.734 KB)

ここでの問題は、次のremotecall_fetch()操作の開始を続ける前に、各remotecall_fetch()操作が完了するまで、つまり各プロセスが作業(この場合は2秒間スリープ)を完了するまでループが待機することです。 。実際の状況では、ここで並列処理のメリットは得られません。これは、プロセスが同時に作業(つまり、スリープ)を行っていないためです。

ただし、@asyncマクロと@syncマクロを組み合わせて使用​​することで、これを修正できます。

@time begin
    a = cell(nworkers())
    @sync for (idx, pid) in enumerate(workers())
        @async a[idx] = remotecall_fetch(sleep, pid, 2)
    end
end
## 2.009416 seconds (274 allocations: 25.592 KB)

ここで、ループの各ステップを個別の操作として数えると、@asyncマクロが前に付いた2つの個別の操作があることがわかります。マクロにより、これらのそれぞれが起動し、コードがそれぞれ終了する前に(この場合はループの次のステップに)続行します。ただし、@syncマクロを使用すると、その範囲はループ全体を包含し、@asyncが前にあるすべての操作が完了するまで、スクリプトがそのループを通過できないようにすることができます。

上記の例をさらに微調整して、特定の変更によってどのように変化するかを確認することにより、これらのマクロの動作をさらに明確に理解することができます。たとえば、@asyncのない@syncがあるとします。

@time begin
    a = cell(nworkers())
    for (idx, pid) in enumerate(workers())
        println("sending work to $pid")
        @async a[idx] = remotecall_fetch(sleep, pid, 2)
    end
end
## 0.001429 seconds (27 allocations: 2.234 KB)

ここで、@asyncマクロを使用すると、各remotecall_fetch()操作の実行が完了する前でもループを継続できます。しかし、良くも悪くも、remotecall_fetch()操作がすべて完了するまでコードがこのループを通過し続けるのを防ぐ@syncマクロはありません。

それでも、remotecall_fetch()の各操作は、たとえ一度実行しても、まだ並行して実行されています。 2秒待つと、結果を含む配列aに次のものが含まれるため、

sleep(2)
Julia> a
2-element Array{Any,1}:
 nothing
 nothing

(「nothing」要素は、sleep関数の結果が正常にフェッチされた結果であり、値を返しません)

また、2つのremotecall_fetch()操作は基本的に同時に開始することもわかります。これらの操作の前にある印刷コマンドも連続して実行されるためです(これらのコマンドからの出力はここには表示されていません)。これを、印刷コマンドが互いに2秒の遅れで実行される次の例と比較してください。

ループ全体に@asyncマクロを(その内部ステップだけでなく)置くと、remotecall_fetch()操作が完了するのを待たずに、スクリプトはすぐに続行されます。ただし、ここでは、スクリプトがループ全体を通過し続けることのみを許可しています。ループの個々のステップが前のステップが完了する前に開始することはできません。そのため、上記の例とは異なり、スクリプトがループの後に進んだ2秒後、結果の配列には、2番目のremotecall_fetch()操作がまだ完了していないことを示す#undefとして1つの要素が残っています。

@time begin
    a = cell(nworkers())
    @async for (idx, pid) in enumerate(workers())
        println("sending work to $pid")
        a[idx] = remotecall_fetch(sleep, pid, 2)
    end
end
# 0.001279 seconds (328 allocations: 21.354 KB)
# Task (waiting) @0x0000000115ec9120
## This also allows us to continue to

sleep(2)

a
2-element Array{Any,1}:
    nothing
 #undef    

そして、当然のことながら、@sync@asyncを隣り合わせに配置すると、各remotecall_fetch()は(同時にではなく)順次実行されますが、それぞれが完了するまでコードを続行しません。言い換えると、これは、sleep(2)@sync @async sleep(2)と本質的に同じように動作するのと同じように、どちらのマクロも配置していない場合と本質的に同じだと思います。

@time begin
    a = cell(nworkers())
    @sync @async for (idx, pid) in enumerate(workers())
        a[idx] = remotecall_fetch(sleep, pid, 2)
    end
end
# 4.019500 seconds (4.20 k allocations: 216.964 KB)
# Task (done) @0x0000000115e52a10

また、@asyncマクロのスコープ内でより複雑な操作を行うことができることにも注意してください。 documentation は、@asyncのスコープ内のループ全体を含む例を示します。

更新:同期マクロのヘルプには、「@async@spawn@spawnat@parallelの動的に囲まれたすべての使用が完了するまで待機する」 「完了」として数えるためには、@syncおよび@asyncマクロのスコープ内でタスクをどのように定義するかが重要です。上記の例の1つを少し変更した以下の例を考えてみます。

@time begin
    a = cell(nworkers())
    @sync for (idx, pid) in enumerate(workers())
        @async a[idx] = remotecall(sleep, pid, 2)
    end
end
## 0.172479 seconds (93.42 k allocations: 3.900 MB)

Julia> a
2-element Array{Any,1}:
 RemoteRef{Channel{Any}}(2,1,3)
 RemoteRef{Channel{Any}}(3,1,4)

前の例の実行には約2秒かかりました。これは、2つのタスクが並行して実行され、スクリプトがそれぞれの関数の実行を完了してから次に進むことを示しています。ただし、この例の時間評価ははるかに低くなっています。その理由は、@syncの目的で、remotecall()操作がワーカーに実行するジョブを送信すると、「終了」したことです。 (ここで得られる配列aには、RemoteRefオブジェクトタイプのみが含まれていることに注意してください。これは、理論的には将来的にフェッチできる特定のプロセスで何かが起こっていることを示しているだけです)。対照的に、remotecall_fetch()操作は、ワーカーからタスクが完了したというメッセージを受け取ったときにのみ「終了」します。

したがって、スクリプトで次に進む前に、ワーカーでの特定の操作が完了していることを確認する方法を探している場合(たとえば、この投稿で説明されているように、 Juliaのリモートプロセッサでタスクが完了するのを待機しています )「完全」と見なされるものと、それを測定してスクリプトで操作する方法について慎重に検討する必要があります。

48