Julia 併發編程 ---- 如何使用 @async 和 @sync

根據官方文檔(https://julia-doc.readthedocs.io/en/latest/manual/parallel-computing/)描述,@sync,@async將任意表達式包裝到任務中,這意味着,對於屬於其範圍內的任何內容,Julia都將開始運行此任務,然後繼續執行腳本中接下來的其他代碼,而不是等待當前任務完成,再去執行接下來的代碼。下面是一些代碼樣例

沒有使用宏:

# 用例1
@time sleep(2) # 2.005766 seconds (13 allocations: 624 bytes)

使用宏:

可以看到,Julia允許腳本繼續(並允許@time宏完全執行),無需等待@async任務(在本例中是休眠兩秒鐘)完成。

#用例2
@time @async sleep(2)
# 0.028081 seconds (2.19 k allocations: 138.969 KiB)
#Task (runnable) @0x0000000009a745d0

相比之下,@sync宏將“等到以下所有宏 @async、@spawn、@spawnat和@parallel 定義的動態封閉都完成爲止 纔會執行。因此,我們看到:

#用例3
@time @sync @async sleep(2)
#2.007233 seconds (2.38 k allocations: 135.692 KiB)
#Task (done) @0x000000002230bc70

在這個簡單的示例中,沒有必要將@async和@sync的都用同時用在單個實例中。但是,@sync可能有用的地方是將@async應用於多個操作,我們希望這些操作都能同時啓動,且無需等待每個操作完成。

例如,我們有多個worker,我們希望讓每個worker同時處理一個任務,然後從這些任務中獲取結果。初始(但不正確)嘗試可能是:

#用例4
using Distributed
cell(N) = Vector{Any}(undef, N)

addprocs(2)
@time begin
    a = cell(nworkers())
    for (idx, pid) in enumerate(workers())
        a[idx] = remotecall_fetch(sleep, pid, 2)
    end
end
##   8.397929 seconds (3.36 k allocations: 198.953 KiB)

這裏的問題是,循環等待每個remotecall_fetch()操作完成,即等待每個進程完成其工作(在本例中爲休眠2秒),然後繼續啓動下一個remotecall_fetch()操作。從實際情況來看,我們並沒有從並行中得到好處,因爲我們的進程並沒有同時完成它們的工作,即它需要睡眠。

但是,我們可以通過使用@async和@sync宏的組合來解決此問題:

#用例5
@time begin
    a = cell(nworkers())
    @sync for (idx, pid) in enumerate(workers())
        @async a[idx] = remotecall_fetch(sleep, pid, 2)
    end
end
## 2.052846 seconds (3.96 k allocations: 252.474 KiB)

現在,如果我們將循環的每個步驟都拆分成一個單獨的計算操作,我們會看到@async宏將for循環中的任務拆分成兩個部分,它允許啓動循環中的每一個任務,並允許代碼在每次循環完成之前繼續循環的下一步。但是,使用@sync宏(其作用域包含整個循環)意味着,在@async作用範圍內的所有操作都完成之前,我們不會允許腳本跳過循環,執行下面的代碼。

通過進一步調整上面的示例,通過查看在某些修改下它是如何變化的,可以更清楚地理解這些宏的操作。例如,假設我們只有@async而沒有@sync:

#用例6
@time begin
    a = cell(nworkers())
    for (idx, pid) in enumerate(workers())
        println("sending work to $pid")
        @async a[idx] = remotecall_fetch(sleep, pid, 2)
    end
end
# sending work to 2
# sending work to 3
# sending work to 4
# sending work to 5
#   0.070505 seconds (2.45 k allocations: 172.755 KiB)

 

在這裏,@async宏允許我們在每個remotecall_fetch()操作完成之前繼續執行循環中下一個計算操作。在循環中所有的remotecall_fetch()完成之前,代碼可以跳過循環執行下面的代碼,因爲我們沒有使用@sync宏來阻止這種情況發生,總的來說這種情況有好有壞。

每個remotecall_fetch()操作仍然並行運行,繼續往下看,如果我們等待兩秒鐘,那麼執行結果的數組a將包含以下內容:

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

(“nothing”元素是sleep函數執行成功時的返回結果,它不返回任何值)

我們還可以通過分析打印命令的日誌,可以看到兩個remotecall_fetch()操作基本上同時啓動,因爲它們前面的打印命令也是快速連續執行(此處未顯示這些命令的輸出)。我們會將此結果與下一個示例進行對比,在下一個示例中,打印命令彼此之間有2秒的延遲執行:

如果我們將@async宏放在整個循環上(而不僅僅是循環內部的某個步驟上),那麼下面代碼執行時,不會等待所有的remotecall_fetch()操作完成,會立即執行循環下面的代碼。但是,現在只允許整個循環腳本作爲一個整體的異步任務。我們不允許循環中的每個單獨步驟在前一個步驟完成之前就開始。因此,與上面的示例不同,在循環腳本運行兩秒後,執行結果數組中有一個元素爲#undef,表示第二個remotecall_fetch()操作仍未完成。

#用例7
@time begin
    a = cell(nworkers())
    @async for (idx, pid) in enumerate(workers())
        println("sending work to $pid")
        a[idx] = remotecall_fetch(sleep, pid, 2)
    end
end
# sending work to 2
# 0.000308 seconds (31 allocations: 2.968 KiB)
# sending work to 3
# Task (runnable) @0x000000000b8805d0

如果我們將@sync和@async放在彼此旁邊,那麼循環中的每個remotecall_fetch()都按順序(而不是同時)運行,但是在每個remotecall_fetch()完成之前,不會繼續執行for循環後面的代碼。換句話說,這基本上等同於如果我們沒有宏,執行過程基本上與 sleep(2)相同,代碼如下:

#用例8
@time begin
    a = cell(nworkers())
    @sync @async for (idx, pid) in enumerate(workers())
        a[idx] = remotecall_fetch(sleep, pid, 2)
    end
end
#4.046351 seconds (18.29 k allocations: 958.188 KiB)
#Task (done) @0x000000000b882e10

 

還要注意,在@async宏的作用域內可能有更復雜的操作。文檔給出了一個示例,其中包含@async範圍內的整個循環。

更新:請記住,sync宏聲明的代碼將“等待@async、@spawn、@spawnat和@parallel的所有動態封閉執行完成”纔會繼續執行下面的代碼。對於代碼什麼時候算“執行完成”,如何定義@sync和@async宏範圍內的任務是很重要。考慮下面的例子,她對上面給出的一個例子做了一個微小改動:

#用例9
@time begin
    a = cell(nworkers())
    @sync for (idx, pid) in enumerate(workers())
        @async a[idx] = remotecall(sleep, pid, 2)
    end
end
#  0.031383 seconds (2.40 k allocations: 154.428 KiB)

前面的示例執行大約用了2秒鐘來,這表明這兩個任務是並行運行的,並且腳本會等待循環中每個任務完成後再繼續執行後面的代碼。然而,這個例子的運行時間要低得多。原因是使用了@async,同時remotecall()操作在向工作進程發送要執行的作業後就算“完成”了,然後繼續循環中下一個計算。(請注意,這裏的結果數組a只包含RemoteRef對象類型,這隻表示某個特定進程正在發生某種事情,理論上可以在將來的某個時間獲取)。相反,remotecall_fetch()操作只有在從工作進程獲取其任務已完成的結果事時才“完成”。

因此,如果您正在尋找方法,以確保在腳本中繼續執行之前,完成workers的某些操作(例如,在本文討論的:在Julia中等待在遠程處理器上完成任務),則有必要仔細考慮任務在什麼情況下算“完成”,以及如何在你的代碼裏測量和執行任務。

 

 

 

 

發佈了94 篇原創文章 · 獲贊 74 · 訪問量 5萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章