Comment et quand utiliser @async et @sync en Julia

j'ai lu documentation pour les @async et @sync macros mais je n'arrive toujours pas à savoir comment et quand les utiliser, et je ne peux pas trouver beaucoup de ressources ou d'exemples pour eux ailleurs sur internet.

mon objectif immédiat est de trouver un moyen de mettre plusieurs travailleurs à faire du travail en parallèle et d'attendre jusqu'à ce qu'ils aient tous terminé de procéder dans mon code. This post:en Attente d'une tâche à remplir sur la télécommande processeur de Julia contient un façon réussie d'accomplir ceci. J'avais pensé qu'il devrait être possible à l'aide de la @async et @sync macros, mais mes échecs initiaux pour accomplir ceci m'ont fait me demander si je comprends correctement comment et quand utiliser ces macros.

17
demandé sur Community 0000-00-00 00:00:00

1 réponses

selon la documentation sous ?@async,"@async encapsule une expression dans une Tâche."Ce que cela signifie, c'est que pour tout ce qui entre dans son champ d'application, Julia va commencer cette tâche en cours d'exécution, puis passer à ce qui vient ensuite dans le script sans attendre que la tâche soit terminée. Ainsi, par exemple, sans la macro vous obtiendrez:

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

Mais avec la macro, vous obtenez:

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

julia> 

Julia permet ainsi au script de continuer (et le @time macro à exécuter pleinement) sans attendre la tâche (dans ce cas, couchage pour deux secondes).

@sync macro, par contraste, va "attendre jusqu'à ce que toutes les utilisations de@async,@spawn,@spawnat et @parallel sont complètes."(selon la documentation sous ?@sync). Ainsi, nous voyons:

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

dans cet exemple simple, il n'y a donc pas de raison d'inclure une seule instance de @async et @sync ensemble. Mais, où @sync peut être utile, c'est l'endroit où vous avez @async appliqué à des opérations multiples que vous souhaitez permettre à tous de commencer à la fois sans attendre pour chacun d'accomplir.

par exemple, supposons que nous ayons plusieurs travailleurs et que nous souhaitions que chacun d'eux commence à travailler simultanément sur une tâche et ensuite récupérer les résultats de ces tâches. Une tentative initiale (mais incorrecte) pourrait être:

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)

Le problème ici est que la boucle attend chaque remotecall_fetch() opération la finition, c'est à dire pour chaque processus d'achever ses travaux (dans ce cas couchage pour 2 secondes) avant de continuer pour démarrer la prochaine remotecall_fetch() opération. En termes de situation pratique, nous n'obtenons pas les avantages du parallélisme ici, puisque nos processus ne font pas leur travail (c.-à-d. dormir) simultanément.

Nous pouvons corriger cela, cependant, en utilisant une combinaison de l' @async et @sync macros:

@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)

maintenant, si on compte chaque pas de la boucle comme un opération distincte, nous voyons qu'il y a deux opérations séparées, précédé par le @async macro. La macro permet à chacun de ces start-up, et le code de continuer (dans ce cas, l'étape suivante de la boucle) avant chaque finitions. Mais, l'utilisation de l' @sync macro, dont la portée englobe toute la boucle, signifie que nous ne permettrons pas au script de passer au-delà de cette boucle jusqu'à ce que toutes les opérations soient précédées de @async avoir terminé.

Il est possible d'obtenir encore plus compréhension claire du fonctionnement de ces macros en modifiant l'exemple ci-dessus pour voir comment il change sous certaines modifications. Par exemple, supposons que nous avons juste le @async sans @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)

Ici @async macro nous permet de continuer dans notre boucle même avant chaque remotecall_fetch() fin de l'opération d'exécution. Mais, pour le meilleur et pour le pire, nous n'avons pas!--13--> macro pour empêcher le code de continuer cette boucle jusqu'à ce que tous les remotecall_fetch() les opérations de finition.

néanmoins, chacun remotecall_fetch() l'opération se déroule toujours en parallèle, même une fois que nous continuons. Nous pouvons le voir car si nous attendons deux secondes, alors le tableau a, contenant les résultats, contiendra:

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

(l'élément" nothing " est le résultat d'un fetch réussi des résultats de la fonction sleep, qui ne renvoie aucune valeur)

On peut aussi voir que les deux remotecall_fetch() les opérations commencent essentiellement au même moment le temps parce que les commandes d'impression qui les précèdent s'exécutent aussi en succession rapide (sortie de ces commandes non montrée ici). Comparez ceci avec l'exemple suivant où les commandes d'impression s'exécutent avec un décalage de 2 secondes l'une par rapport à l'autre:

si on met le @async macro sur l'ensemble de la boucle (au lieu de simplement l'étape interne de celle-ci), puis à nouveau notre script continuera immédiatement sans attendre le remotecall_fetch() opérations à terminer. Maintenant, cependant, nous n'autorisons que le script à continuer au-delà de la boucle dans son ensemble. Nous ne permettons pas que chaque étape de la boucle commence avant la fin de la précédente. En tant que tel, contrairement à l'exemple ci-dessus, deux secondes après que le script passe après la boucle, il y a le tableau de résultats qui a encore un élément comme #undef indiquant que le second remotecall_fetch() l'opération n'est toujours pas terminée.

@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    

Et, il n'est pas surprenant, si l'on met l' @sync et @async à côté de l'autre, nous obtenons que chaque remotecall_fetch() fonctionne séquentiellement (plutôt que simultanément) mais nous ne continuons pas dans le code jusqu'à ce que chacun ait terminé. En d'autres termes, ce serait, je crois, essentiellement l'équivalent de si nous n'avions pas de macro en place, tout comme sleep(2) se comporte essentiellement de façon identique à @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

Notez aussi qu'il est possible d'avoir des opérations plus compliquées à l'intérieur de la portée de l' @async macro. documentation donne un exemple contenant une boucle entière à l'intérieur le champ d'application de l' @async.

mise à Jour: rappelons que l'aide pour les macros de synchronisation stipule qu'elle " attendra jusqu'à ce que toutes les utilisations de @async,@spawn,@spawnat et @parallel sont complètes."Aux fins de ce qui est considéré comme "complet", il importe de savoir comment vous définissez les tâches dans le cadre de la @sync et @async macros. Considérons l'exemple ci-dessous, qui est une légère variation sur l'un des exemples donnés ci-dessus:

@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)

il a fallu environ 2 secondes pour exécuter l'exemple précédent, ce qui indique que les deux tâches ont été exécutées en parallèle et que le script attendait que chacune termine l'exécution de ses fonctions avant de procéder. Dans cet exemple, toutefois, l'évaluation du temps est beaucoup plus courte. La raison en est que pour les besoins de@syncremotecall() l'opération est "terminée" une fois qu'elle a envoyé au travailleur le travail à faire. (Notez que le tableau résultant, a, ici, ne contient que RemoteRef les types d'objets, qui indiquent simplement qu'il se passe quelque chose avec un processus particulier qui pourrait en théorie être récupéré à un moment donné dans le futur). En revanche, le remotecall_fetch() l'opération n'est" terminée " que lorsqu'elle reçoit du travailleur le message que sa tâche est terminée.

ainsi, si vous cherchez des moyens de vous assurer que certaines opérations avec les ouvriers ont été effectuées avant de passer à autre chose dans votre script (comme par exemple est discuté dans ce post:en Attente d'une tâche à accomplir sur le processeur à distance en Julia) il est nécessaire de bien réfléchir à ce qui est considéré comme "complet" et à la façon de le mesurer et de l'opérationnaliser dans votre script.

38
répondu 2018-04-07 16:52:49