16

Ho letto il documentation per i macro @async e @sync ma non riesco ancora a capire come e quando usarli, né posso trovare molte risorse o esempi per loro altrove su la rete.Come e quando usare @async e @sync in Julia

Il mio obiettivo immediato è quello di trovare un modo per impostare diversi lavoratori a lavorare in parallelo e quindi attendere che abbiano finito di procedere nel mio codice. Questo post: Waiting for a task to be completed on remote processor in Julia contiene un modo efficace per raggiungere questo obiettivo. Avevo pensato che sarebbe stato possibile utilizzare i macro @async e @sync, ma i miei primi fallimenti nel realizzare ciò mi hanno fatto riflettere se capisco correttamente come e quando utilizzare queste macro.

risposta

32

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.

+2

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 –

+7

Una bella risposta! – StefanKarpinski