16

我已阅读documentation中的@async@sync宏,但仍无法弄清楚如何以及何时使用它们,我也无法在其他地方找到许多资源或示例互联网。如何以及何时在Julia中使用@async和@sync

我的目标是找到一种方法来设置几个工作人员并行工作,然后等待他们完成了所有工作,然后继续执行我的代码。这篇文章:Waiting for a task to be completed on remote processor in Julia包含一个成功的方法来实现这一点。我曾认为应该可以使用@async@sync宏,但是我最初的失败使我不知道我是否正确理解如何以及何时使用这些宏。

回答

32

根据[email protected]下的文档,“@async在任务中包装表达式”。这意味着,对于任何属于其范围内的内容,Julia都会开始执行此任务,然后继续处理脚本中的下一个任务,而无需等待任务完成。因此,举例来说,如果没有宏观您将获得:

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> 

朱莉娅从而允许脚本继续(与@time宏完全执行)无需等待任务(在这种情况下,睡眠两秒钟)即可完成。

@sync宏,相比之下,将“等到所有动态封闭的@async@spawn@spawnat@parallel用途是完整的。” (根据[email protected]下的文件)。于是,我们看到:

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

在这个简单的例子然后,没有指向包括@async@sync单个实例在一起。但是,在@sync可用的情况下,您可以将@async应用于您希望允许所有人一次启动而不等待每个完成的多个操作。

例如,假设我们有多个工作人员,我们希望他们每个人都同时开展一项任务,然后从这些任务中获取结果。最初的(但不正确的)尝试可能是:

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) 

这里的问题是,循环等待每个remotecall_fetch()操作完成,即对于每一个进程完成其工作(在这种情况下睡2秒),然后继续开始下一个remotecall_fetch()操作。就实际情况而言,我们并没有从这里获得并行的好处,因为我们的流程并没有同时工作(即睡眠)。

我们可以但是解决这个问题,通过使用@async@sync宏的组合:现在

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

,如果再算上循环作为一个单独操作的每一个步骤,我们可以看到,有两个以@async之前的宏进行单独的操作。宏允许每个启动,并且代码在每个完成之前继续执行(在本例中为循环的下一步)。但是,使用范围包含整个循环的@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(pid, sleep, 2) 
    end 
end 
## 0.001429 seconds (27 allocations: 2.234 KB) 

这里,@async宏允许我们甚至执行完毕之前,各remotecall_fetch()操作在我们的循环继续。但是,无论好坏,我们没有@sync宏,以防止代码继续经过这个循环,直到所有remotecall_fetch()操作完成。尽管如此,即使我们继续,每个remotecall_fetch()操作仍然在并行运行。我们可以看到,因为如果我们等待2秒钟,然后阵列,包含结果,将包含:

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

(在“无”元素是一个成功的结果取睡眠的结果函数,它不返回任何值)

我们还可以看到,两个remotecall_fetch()操作基本上同时启动,因为它们之前的打印命令也是快速连续执行的(这些命令的输出未在此处显示)。与下一个例子中的打印命令以相差2秒的延迟进行对比:

如果我们将@async宏放在整个循环(而不是它的内部步骤)上,那么我们的脚本将再次立即继续而不等待remotecall_fetch()操作完成。但是,现在我们只允许脚本作为一个整体继续循环。我们不允许循环的每个单独步骤在前一个完成之前启动。因此,与上面的示例不同,脚本在循环之后两秒钟后,结果数组仍然有一个元素作为#undef,指示第二个remotecall_fetch()操作仍未完成。

@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  

而且,这并不奇怪,如果我们把@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(pid, sleep, 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宏的范围内定义任务。考虑下面的例子,这是上面给出的实施例之一的轻微变化:

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

前面的示例大约花费2秒执行,这表明这两个任务是在并行运行和脚本等待每个人在继续之前完成其功能的执行。然而,这个例子的评估时间要少得多。原因在于,为了达到@sync的目的,remotecall()操作在它向该工作人员发送该作业后“完成”。 (请注意,结果数组a仅包含RemoteRef对象类型,它们只是表明某个特定进程正在发生某些进程,理论上这些进程可能在将来的某个时间点进行提取)。相比之下,remotecall_fetch()操作在从任务完成的工作人员处收到消息时才会“完成”。

因此,如果您正在寻找方法以确保在您的脚本中继续进行某些与工作人员的操作之前(例如在本文中讨论:Waiting for a task to be completed on remote processor in Julia),有必要仔细考虑什么算作“完成“以及如何测量并在脚本中操作。

+2

这篇文章的灵感来自于@FelipeLema在这篇文章中的有用答案和讨论:http://stackoverflow.com/questions/32143159/waiting-for-a-task-to-be-completed-on-remote-处理器在茱莉亚/ 32148849#32148849 –

+7

一个可爱的答案! – StefanKarpinski

相关问题