In base alla documentazione di [email protected]
, "@async
avvolge un'espressione in un'attività." Ciò significa che per qualsiasi cosa rientri nel suo ambito, Julia avvierà questa attività in esecuzione, ma poi procederà a ciò che viene dopo nello script senza attendere il completamento dell'attività. Così, ad esempio, senza la macro si otterrà:
julia> @time sleep(2)
2.005766 seconds (13 allocations: 624 bytes)
Ma con la macro, si ottiene:
julia> @time @async sleep(2)
0.000021 seconds (7 allocations: 657 bytes)
Task (waiting) @0x0000000112a65ba0
julia>
Julia permette in tal modo lo script di procedere (e la @time
macro per eseguire completamente) senza attendere il completamento del compito (in questo caso, dormire per due secondi).
Il @sync
macro, al contrario, si "Attendere fino a quando tutti gli usi in modo dinamico-chiusi della @async
, @spawn
, @spawnat
e @parallel
sono completi." (secondo la documentazione sotto [email protected]
). Così, vediamo:
julia> @time @sync @async sleep(2)
2.002899 seconds (47 allocations: 2.986 KB)
Task (done) @0x0000000112bd2e00
In questo semplice esempio, allora, non v'è alcun punto di includere una singola istanza di @async
e @sync
insieme. Tuttavia, laddove @sync
può essere utile, è possibile applicare @async
a più operazioni che si desidera consentire a tutti di avviarsi in una sola volta senza attendere il completamento di ciascuna.
Ad esempio, supponiamo di avere più lavoratori e vorremmo iniziare ognuno di loro a lavorare su un'attività contemporaneamente e quindi recuperare i risultati da tali attività. Un primo tentativo (ma non corretto) potrebbe essere:
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)
Il problema è che il ciclo attende ogni operazione remotecall_fetch() per terminare, cioè per ogni processo per completare il lavoro (in questo caso letto per 2 secondi) prima di continuare ad avviare la prossima operazione remotecall_fetch(). In termini di situazione pratica, qui non stiamo ottenendo i vantaggi del parallelismo, poiché i nostri processi non stanno facendo il loro lavoro (cioè dormendo) simultaneamente.
Possiamo correggere questo, però, utilizzando una combinazione dei @async
e @sync
macro:
@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)
Ora, se contiamo ogni fase del ciclo come un'operazione separata, vediamo che ci sono due operazioni separate precedute dalla macro @async
. La macro consente a ciascuno di questi di avviarsi e il codice per continuare (in questo caso al prossimo passo del ciclo) prima di ogni finitura.Tuttavia, l'uso della macro @sync
, il cui ambito comprende l'intero ciclo, significa che non consentiremo allo script di procedere oltre quel ciclo finché tutte le operazioni precedute da @async
non sono state completate.
È possibile ottenere una comprensione ancora più chiara dell'operazione di queste macro modificando ulteriormente l'esempio precedente per vedere come cambia in determinate modifiche. Per esempio, supponiamo ci resta che la @async
senza la @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)
Qui, la macro @async
ci permette di continuare nel nostro circuito, anche prima di ogni operazione remotecall_fetch() Finiture esecuzione. Ma, nel bene o nel male, non abbiamo la macro @sync
per impedire che il codice continui oltre questo ciclo fino a quando tutte le operazioni di remotecall_fetch() terminano.
Tuttavia, ogni operazione di remotecall_fetch() è ancora in esecuzione in parallelo, anche una volta che si va avanti. Possiamo vedere che perché se aspettiamo per due secondi, poi l'array una, contenente i risultati, conterrà:
sleep(2)
julia> a
2-element Array{Any,1}:
nothing
nothing
(L'elemento di "nulla" è il risultato di un successo recuperare i risultati del sonno funzione, che non restituisce alcun valore)
Possiamo anche vedere che le due operazioni di remotecall_fetch() iniziano essenzialmente nello stesso momento perché i comandi di stampa che li precedono vengono eseguiti in rapida successione (output da questi comandi non mostrato qui). Confrontalo con il prossimo esempio in cui i comandi di stampa vengono eseguiti a un intervallo di 2 secondi l'uno dall'altro:
Se inseriamo la macro @async
sull'intero ciclo (anziché solo il suo passo interno), di nuovo il nostro script continua immediatamente senza aspettare che le operazioni di remotecall_fetch() finiscano. Ora, tuttavia, consentiamo allo script di continuare oltre il ciclo nel suo complesso. Non permettiamo che ogni singola fase del ciclo inizi prima della precedente. Pertanto, a differenza dell'esempio precedente, due secondi dopo che lo script procede dopo il ciclo, l'array dei risultati contiene ancora un elemento come #undef che indica che la seconda operazione remotecall_fetch() non è ancora stata completata.
@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
E, non a caso, se mettiamo la @sync
e @async
proprio accanto all'altra, si ottiene che ogni remotecall_fetch() viene eseguito in sequenza (piuttosto che allo stesso tempo), ma non continuiamo nel codice fino a che ogni ha finito. In altre parole, questo sarebbe, ritengo, sostanzialmente equivalente se avessimo né macro in luogo, come sleep(2)
si comporta essenzialmente identico al @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
noti inoltre che è possibile avere operazioni più complesse all'interno l'ambito della macro @async
. Il numero documentation fornisce un esempio contenente un intero ciclo nell'ambito di @async
.
Update: "Attendere fino a quando tutti gli usi in modo dinamico-chiusi della @async
, @spawn
, @spawnat
e @parallel
sono completi" Ricordiamo che l'aiuto per le macro di sincronizzazione dichiara che Ai fini di ciò che conta come "completo" importa come si definiscono le attività nell'ambito delle macro @sync
e @async
.Si consideri l'esempio riportato di seguito, che è una leggera variazione su uno degli esempi di cui sopra:
@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)
L'esempio precedente sono voluti circa 2 secondi per eseguire, indicando che i due compiti sono stati eseguiti in parallelo e che lo script in attesa di ciascuno per completare l'esecuzione delle proprie funzioni prima di procedere. Questo esempio, tuttavia, ha una valutazione del tempo molto più bassa. Il motivo è che ai fini di @sync
l'operazione remotecall() ha "finito" una volta che ha inviato al lavoratore il lavoro da eseguire. (Si noti che l'array risultante, a, qui, contiene solo tipi di oggetto RemoteRef, che indicano solo che c'è qualcosa in corso con un particolare processo che in teoria potrebbe essere recuperato in qualche punto in futuro). Al contrario, l'operazione remotecall_fetch() ha solo "finito" quando riceve il messaggio dal lavoratore che la sua attività è completa.
Quindi, se stai cercando dei modi per assicurarti che certe operazioni con i lavoratori siano state completate prima di passare allo script (come ad esempio è discusso in questo post: Waiting for a task to be completed on remote processor in Julia) è necessario riflettere attentamente su ciò che conta come " completo "e come misurerai e poi renderà operativo quello nel tuo script.
Questo post è stato ispirato dalle utili risposte e discussioni di @FelipeLema in questo post: http://stackoverflow.com/questions/32143159/waiting-for-a-task-to-be-completed-on-remote- processor-in-julia/32148849 # 32148849 –
Una bella risposta! – StefanKarpinski