如何以及何时在Julia中使用@async和@sync

时间:2016-05-17 22:07:36

标签: asynchronous parallel-processing macros synchronization julia

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

我的直接目标是找到一种方法来设置多个工作人员并行工作,然后等到他们全部完成后继续我的代码。这篇文章:Waiting for a task to be completed on remote processor in Julia包含一个成功完成此任务的方法。我原以为应该可以使用@async@sync宏,但是我最初的失败让我想知道我是否正确理解了如何以及何时使用这些宏。

1 个答案:

答案 0 :(得分:44)

根据?@async下的文档," @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> 
因此,Julia允许脚本继续(并且@time宏完全执行)而无需等待任务(在这种情况下,休眠两秒钟)完成。

相比之下,@sync宏会等到@async@spawn@spawnat@parallel的所有动态封闭使用完整。" (根据?@sync下的文件)。因此,我们看到:

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()操作仍然会并行运行。我们可以看到,因为如果我们等待两秒钟,那么包含结果的数组a将包含:

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()按顺序(而不是同时)运行,但我们不会#39 ; t继续在代码中,直到每个完成。换句话说,我认为,如果我们既没有宏,也就是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,{{ 1}}和@spawnat已完成。"为了达到"完成"重要的是如何在@parallel@sync宏的范围内定义任务。考虑下面的例子,这是上面给出的一个例子的略微变化:

@async

前面的示例大约需要2秒钟才能执行,表明这两个任务是并行运行的,并且脚本在继续之前等待每个任务完成其功能的执行。然而,这个例子的评估时间要低得多。原因是,为了@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) @sync操作已经完成了"一旦它发送了工人的工作。 (请注意,结果数组a在这里只包含RemoteRef对象类型,它们只表示特定进程正在发生某些事情,理论上可以在将来的某个时刻获取)。相比之下,remotecall()操作只有"完成"当它从工作者那里得到它的任务完成的消息时。

因此,如果您正在寻找方法来确保在继续编写脚本之前已完成对工作人员的某些操作(例如在本文中讨论:Waiting for a task to be completed on remote processor in Julia),则需要仔细考虑算作"完成"以及如何衡量并在脚本中对其进行操作。