16

Я прочитал documentation для @async и @sync макросов, но до сих пор не могу понять, как и когда использовать их, и я не могу найти много ресурсов или примеры для них в других местах интернет.Как и когда использовать @async и @sync в Джулии

Моя ближайшая цель - найти способ установить несколько рабочих для параллельной работы, а затем подождать, пока они не закончат работу в моем коде. Этот пост: Waiting for a task to be completed on remote processor in Julia содержит один успешный способ сделать это. Я думал, что это должно быть возможно с использованием макросов и @sync, но мои первоначальные неудачи для достижения этого заставили меня задаться вопросом, правильно ли я понимаю, как и когда использовать эти макросы.

ответ

32

В соответствии с документацией под номером [email protected], «@async обертывает выражение в задаче». Это означает, что для того, что попадает в сферу его охвата, Джулия начнет выполнение этой задачи, а затем перейдет к тому, что будет дальше в скрипте, не дожидаясь завершения задачи. Так, например, без макроса вы получите:

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 и не является полным.» (в соответствии с документацией под номером [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() все еще работает параллельно, даже когда мы идем дальше. Мы можем видеть, что, потому что если мы будем ждать в течение двух секунд, затем массив а, содержащий результаты, будет содержать:

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.

Update: Напомним, что помощь для синхронизации макросов утверждает, что она не будет «Подождите, пока все динамически закрытых использования @async, @spawn, @spawnat@parallel и являются полными.» Для целей, которые считаются «полными», важно, как вы определяете задачи в рамках макросов и @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() была «закончена», как только она отправила работника на выполнение задания. (Обратите внимание, что результирующий массив, здесь, просто содержит типы объектов 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- процессор-in-julia/32148849 # 32148849 –

+7

Прекрасный ответ! – StefanKarpinski

Смежные вопросы