16

ich die documentation für die @async und @sync Makros gelesen haben, aber immer noch nicht herausfinden können, wie und wann sie verwenden, noch kann ich viele Ressourcen oder Beispiele für sie an anderer Stelle finden auf das Internet.Wann und wie @async und @sync in Julia verwenden

Mein direktes Ziel ist es, einen Weg zu finden, mehrere Arbeiter parallel arbeiten zu lassen und dann zu warten, bis alle fertig sind, um in meinem Code fortzufahren. Dieser Beitrag: Waiting for a task to be completed on remote processor in Julia enthält einen erfolgreichen Weg, dies zu erreichen. Ich hatte gedacht, dass es mit den Makros @async und @sync möglich sein sollte, aber meine anfänglichen Fehler, dies zu erreichen, ließen mich fragen, ob ich richtig verstehe, wie und wann diese Makros zu verwenden sind.

Antwort

32

Entsprechend der Dokumentation unter [email protected], "@async umschließt einen Ausdruck in einer Aufgabe." Was das bedeutet, ist, dass Julia, egal was in ihren Bereich fällt, diese Aufgabe startet, aber dann weitergeht zu dem, was als nächstes im Skript kommt, ohne auf den Abschluss der Aufgabe zu warten. So zum Beispiel, ohne das Makro erhalten Sie:

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

Aber mit dem Makro, erhalten Sie:

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

julia> 

Julia ermöglicht somit das Skript zu gehen (und das @time Makro vollständig auszuführen) ohne auf die Aufgabe zu warten (in diesem Fall für zwei Sekunden zu schlafen).

Das @sync Makro, dagegen wird „Warten Sie, bis alle dynamisch geschlossenen Verwendungen von @async, @spawn, @spawnat und @parallel abgeschlossen sind.“ (laut Dokumentation unter [email protected]). So sehen wir:

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

In diesem einfachen Beispiel dann gibt es keinen Punkt ist eine einzelne Instanz @async und @sync zusammen einschließlich. Aber wo @sync kann nützlich sein, wo Sie haben @async für mehrere Operationen, die Sie alle gleichzeitig starten möchten, ohne zu warten, dass jeder abgeschlossen.

Angenommen, wir haben mehrere Arbeiter und möchten jeden von ihnen gleichzeitig an einer Aufgabe arbeiten lassen und dann die Ergebnisse dieser Aufgaben abrufen. Ein erster (aber falsch) Versuch könnte sein:

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

Das Problem hierbei ist, dass die Schleife für jeden remotecall_fetch() Operation wartet, für jeden Prozess seiner Arbeit abzuschließen, dh zu beenden (in diesem Fall für 2 Sekunden Schlaf) bevor mit dem nächsten remotecall_fetch() - Vorgang fortgefahren wird. In Bezug auf eine praktische Situation erhalten wir hier nicht die Vorteile der Parallelität, da unsere Prozesse nicht gleichzeitig ihre Arbeit verrichten (d. H. Schlafen).

Wir dies korrigieren kann jedoch durch eine Kombination der @async und @sync Makros:

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

Nun, wenn wir jeden Schritt der Schleife als separater Betrieb zählen, sehen wir, dass es zwei separate Operationen, denen das Makro @async vorausgeht. Das Makro ermöglicht, dass jedes von diesen gestartet wird, und der Code wird fortgesetzt (in diesem Fall zum nächsten Schritt der Schleife), bevor jeder beendet ist.Aber die Verwendung des Makros @sync, dessen Gültigkeitsbereich die gesamte Schleife umfasst, bedeutet, dass das Skript nicht über diese Schleife hinausgehen darf, bis alle Operationen abgeschlossen sind, denen @async vorausgegangen ist.

Es ist möglich, ein noch klareres Verständnis der Funktionsweise dieser Makros zu erhalten, indem man das obige Beispiel weiter optimiert, um zu sehen, wie es sich unter bestimmten Modifikationen ändert. Zum Beispiel: Angenommen wir haben nur die @async ohne @sync:

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

Hier wird das @async Makro ermöglicht es uns, in unserer Schleife fortzusetzen, selbst vor jeder remotecall_fetch() Operation beendet haben sollte. Aber zum Besseren oder Schlechteren haben wir kein Makro @sync, um zu verhindern, dass der Code über diese Schleife hinausgeht, bis alle remotecall_fetch() -Operationen abgeschlossen sind.

Nichtsdestotrotz läuft jede remotecall_fetch() Operation noch parallel, auch wenn wir weitermachen. Wir können sehen, dass, weil, wenn wir für zwei Sekunden warten, dann das Array ein, die Ergebnisse enthalten, beinhalten:

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

(Das „Nichts“ Element das Ergebnis eines erfolgreich ist von den Ergebnissen der Schlaf holen (Funktion, die keine Werte zurückgibt)

Wir können auch sehen, dass die zwei remotecall_fetch() Operationen im Wesentlichen zur gleichen Zeit starten, weil die Druckbefehle, die ihnen vorausgehen, auch in schneller Folge ausgeführt werden (Ausgabe von diesen Befehlen hier nicht gezeigt)). Vergleichen Sie dies mit dem nächsten Beispiel, wo die Druckbefehle mit einer Verzögerung von 2 Sekunden ausgeführt werden:

Wenn wir das Makro @async auf die ganze Schleife setzen (statt nur den inneren Schritt davon), dann wird unser Skript wieder Fahren Sie sofort fort, ohne zu warten, bis die remotecall_fetch() -Operationen abgeschlossen sind. Jetzt erlauben wir jedoch nur, dass das Skript als Ganzes über die Schleife hinausgeht. Wir lassen nicht zu, dass jeder einzelne Schritt der Schleife beginnt, bevor der vorherige abgeschlossen ist. Daher gibt es, anders als im obigen Beispiel, zwei Sekunden, nachdem das Skript nach der Schleife fortschreitet, das Ergebnisarray, das immer noch ein Element als #undef anzeigt, das anzeigt, dass die zweite remotecall_fetch() -Operation noch nicht abgeschlossen ist.

@time begin 
    a = cell(nworkers()) 
    @async for (idx, pid) in enumerate(workers()) 
     println("sending work to $pid") 
     a[idx] = remotecall_fetch(pid, sleep, 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  

Und, nicht überraschend, wenn wir setzen die @sync und @async direkt nebeneinander, wir bekommen, dass jede remotecall_fetch() läuft nacheinander (nicht gleichzeitig), aber wir nicht weiter in den Code, bis jeder hat beendet. Mit anderen Worten, wäre dies, glaube ich, im Wesentlichen entspricht, wenn wir weder Makro an Ort und Stelle hatten, genau wie sleep(2) zu @sync @async sleep(2)

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

Hinweis im Wesentlichen identisch verhält sich auch, dass es möglich ist, kompliziertere Operationen nach innen haben der Umfang der @async Makro. Die documentation gibt ein Beispiel, das eine gesamte Schleife innerhalb des Umfangs von @async enthält.

Update: „, bis alle dynamisch geschlossenen Anwendungen von @async, Warten @spawn, @spawnat und @parallel sind abgeschlossen“ Recall, dass die Hilfe für die Sync-Makros besagt, dass es Für die Zwecke dessen, was als "vollständig" gilt, ist es wichtig, wie Sie die Aufgaben im Rahmen der Makros @sync und @async definieren.Betrachten Sie das folgende Beispiel, die eine leichte Variation oben auf eines der Beispiele ist gegeben:

@time begin 
    a = cell(nworkers()) 
    @sync for (idx, pid) in enumerate(workers()) 
     @async a[idx] = remotecall(pid, sleep, 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) 

Das frühere Beispiel nahm etwa 2 Sekunden ausgeführt werden, was darauf hinweist, dass die beiden Aufgaben parallel ausgeführt wurden und dass das Skript warten jeweils um die Ausführung ihrer Funktionen abzuschließen, bevor sie fortfahren. Dieses Beispiel hat jedoch eine viel geringere Zeitauswertung. Der Grund dafür ist, dass für den Zweck von @sync die remotecall() - Operation "beendet" ist, sobald sie dem Arbeiter den Job gesendet hat. (Beachten Sie, dass das resultierende Array, a, hier nur RemoteRef-Objekttypen enthält, die lediglich anzeigen, dass bei einem bestimmten Prozess etwas im Gange ist, das theoretisch zu irgendeinem Zeitpunkt in der Zukunft abgerufen werden könnte). Im Gegensatz dazu hat die remotecall_fetch() Operation nur "beendet", wenn sie die Nachricht vom Worker erhält, dass ihre Aufgabe abgeschlossen ist.

So, wenn Sie nach Möglichkeiten suchen, sicherzustellen, dass bestimmte Operationen mit Arbeitern abgeschlossen haben, bevor Sie in Ihrem Skript weitermachen (wie zum Beispiel in diesem Post diskutiert: Waiting for a task to be completed on remote processor in Julia) ist es notwendig, sorgfältig darüber nachzudenken, was zählt als " vervollständigen "und wie Sie das in Ihrem Skript messen und dann operationalisieren.

+2

Dieser Beitrag wurde von den hilfreichen Antworten und Diskussionen von @FelipeLema in diesem Beitrag inspiriert: http://StackOverflow.com/Questions/32143159/waiting-for-a-task-to-be-completed-on-remote- Prozessor-in-Julia/32148849 # 32148849 –

+7

Eine schöne Antwort! – StefanKarpinski