Поток управления
Julia предоставляет различные конструкции для управления потоком:
-
Составные выражения:
beginи(;). -
Условное вычисление:
if-elseif-elseи?:(тернарный оператор). -
Короткое замыкание:
&&,||и цепочечные сравнения. -
Повторяющееся вычисление: циклы:
whileиfor. -
Обработка исключений:
try-catch,error()иthrow(). -
Задачи (также корутины):
yieldto().
Первые пять механизмов управления потоком являются стандартными для языков высокого уровня программирования. Task не являются стандартными: они обеспечивают нелокальный поток управления, позволяя переключаться между временно приостановленными вычислениями. Это мощная конструкция: как обработка исключений, так и кооперативная многозадачность реализованы в Julia с использованием задач. Для повседневной программирования прямое использование задач не требуется, но некоторые задачи могут быть решены значительно проще с помощью задач.
Составные выражения
Иногда удобно иметь одно выражение, которое вычисляет несколько подвыражений в порядке их следования, возвращая значение последнего подвыражения как своё собственное значение. Julia имеет две конструкции для этого: блоки begin и цепочки (;). Значение обеих конструкций составных выражений — это значение последнего подвыражения. Вот пример блока begin.
julia> z = begin
x = 1
y = 2
x + y
end
3
Поскольку это довольно небольшие и простые выражения, их легко разместить на одной строке, где пригодится синтаксис цепочки (;).
julia> z = (x = 1; y = 2; x + y) 3
Этот синтаксис особенно полезен с краткой формой определения функций на одной строке, представленной в Функциях. Хотя это типичный случай, нет требования, чтобы блоки begin были многострочными, или что цепочки (;) были однострочными:
julia> begin x = 1; y = 2; x + y end
3
julia> (x = 1;
y = 2;
x + y)
3
Условное вычисление
Условное вычисление позволяет оценивать части кода или не оценивать их в зависимости от значения булевого выражения. Вот анатомия условного синтаксиса if-elseif-else:
if x < y
println("x is less than y")
elseif x > y
println("x is greater than y")
else
println("x is equal to y")
end
Если выражение условия x < y имеет значение true, то соответствующий блок вычисляется; в противном случае вычисляется выражение условия x > y, и если оно имеет значение true, соответствующий блок вычисляется; если ни одно выражение не истинно, вычисляется блок else. Вот как это работает:
julia> function test(x, y)
if x < y
println("x is less than y")
elseif x > y
println("x is greater than y")
else
println("x is equal to y")
end
end
test (generic function with 1 method)
julia> test(1, 2)
x is less than y
julia> test(2, 1)
x is greater than y
julia> test(1, 1)
x is equal to y
Блоки elseif и else необязательны, и можно использовать любое количество блоков elseif. Выражения условий в конструкции if-elseif-else вычисляются до тех пор, пока первое из них не примет значение true, после чего вычисляется связанный с ним блок, и больше никаких выражений условий или блоков не вычисляются.
Блоки if являются «проницаемыми», т. е. они не вводят локальную область видимости. Это означает, что новые переменные, определённые внутри блоков ìf, могут использоваться после блока if, даже если они не были определены до этого. Таким образом, мы могли бы определить функцию test выше как
julia> function test(x,y)
if x < y
relation = "less than"
elseif x == y
relation = "equal to"
else
relation = "greater than"
end
println("x is ", relation, " y.")
end
test (generic function with 1 method)
Переменная relation объявлена внутри блока if, но используется снаружи. Однако, если вы полагаетесь на это поведение, убедитесь, что все возможные пути кода определяют значение для переменной. Следующее изменение функции выше приводит к ошибке во время выполнения
julia> function test(x,y)
if x < y
relation = "less than"
elseif x == y
relation = "equal to"
end
println("x is ", relation, " y.")
end
test (generic function with 1 method)
julia> test(1,2)
x is less than y.
julia> test(2,1)
ERROR: UndefVarError: relation not defined
in test(::Int64, ::Int64) at ./none:7
...
Блоки if также возвращают значение, что может показаться непонятным пользователям, пришедшим из многих других языков программирования. Это значение просто возвращаемое значение последней выполненной инструкции в выбранном ответвлении, так что
julia> x = 3
3
julia> if x > 0
"positive!"
else
"negative..."
end
"positive!"
Обратите внимание, что очень короткие условные операторы (однострочные) часто выражаются с использованием короткого замыкания в Julia, как описано в следующем разделе.
В отличие от C, MATLAB, Perl, Python и Ruby — но как Java и некоторых других более строгих языков с типизацией — ошибка возникает, если значение выражения условия не равно true или false:
julia> if 1
println("true")
end
ERROR: TypeError: non-boolean (Int64) used in boolean context
...
Эта ошибка указывает на то, что условие имело неверный тип: Int64 вместо требуемого Bool.
Так называемый «тернарный оператор» ?: тесно связан с синтаксисом if-elseif-else, но используется там, где требуется условный выбор между значениями одиночных выражений, в отличие от условного выполнения более длинных блоков кода. Он получил своё название от того, что в большинстве языков он является единственным оператором, принимающим три операнда:
a ? b : c
Выражение a, перед ?, является выражением условия, и операция с тремя операндами оценивает выражение b, перед :, если условие a имеет значение true, или выражение c, после :, если оно имеет значение false.
Самый простой способ понять это поведение — посмотреть пример. В предыдущем примере вызов println используется всеми тремя ветвями: единственный реальный выбор — какая строковая константа должна быть напечатана. Это можно записать более лаконично, используя тернарный оператор. Для ясности сначала рассмотрим двухстороннюю версию:
julia> x = 1; y = 2; julia> println(x < y ? "less than" : "not less than") less than julia> x = 1; y = 0; julia> println(x < y ? "less than" : "not less than") not less than
Если выражение x < y истинно, то всё выражение тернарного оператора примет значение строки "less than", в противном случае оно примет значение строки "not less than" . Исходный трёхсторонний пример требует объединения нескольких применений тернарного оператора вместе:
julia> test(x, y) = println(x < y ? "x is less than y" :
x > y ? "x is greater than y" : "x is equal to y")
test (generic function with 1 method)
julia> test(1, 2)
x is less than y
julia> test(2, 1)
x is greater than y
julia> test(1, 1)
x is equal to y
Для облегчения объединения оператор связывается справа налево.
Важно, что, как и в if-elseif-else, выражения перед и после : оцениваются только в том случае, если выражение условия оценивается как true или false, соответственно:
julia> v(x) = (println(x); x)
v (generic function with 1 method)
julia> 1 < 2 ? v("yes") : v("no")
yes
"yes"
julia> 1 > 2 ? v("yes") : v("no")
no
"no"
Короткое замыкание
Короткое замыкание очень похоже на условное вычисление. Это поведение встречается в большинстве императивных языков программирования, имеющих булевы операторы && и ||: в последовательности булевых выражений, связанных этими операторами, оценивается только минимальное количество выражений, необходимое для определения окончательного булевого значения всего выражения. Явно это означает, что:
- В выражении
a && b, подвыражениеbоценивается только в том случае, еслиaоценивается какtrue. - В выражении
a || b, подвыражениеbоценивается только в том случае, еслиaоценивается какfalse.
Это потому, что a && b должно быть false, если a имеет значение false, независимо от значения b, и аналогично, значение a || b должно быть истинным, если a имеет значение true, независимо от значения b . Оба оператора && и || связываются справа налево, но && имеет более высокий приоритет, чем || . Легко провести эксперимент с этим поведением:
julia> t(x) = (println(x); true) t (generic function with 1 method) julia> f(x) = (println(x); false) f (generic function with 1 method) julia> t(1) && t(2) 1 2 true julia> t(1) && f(2) 1 2 false julia> f(1) && t(2) 1 false julia> f(1) && f(2) 1 false julia> t(1) || t(2) 1 true julia> t(1) || f(2) 1 true julia> f(1) || t(2) 1 2 true julia> f(1) || f(2) 1 2 false
Вы легко можете провести аналогичные эксперименты с ассоциативностью и приоритетом различных комбинаций операторов && и ||.
Это поведение часто используется в Julia для создания альтернативы очень коротким операторам if. Вместо if <cond> <statement> end, можно написать <cond> && <statement> (что можно прочитать как: <условие> и затем <утверждение>). Аналогично, вместо if ! <cond> <statement> end, можно написать <cond> || <statement> (что можно прочитать как: <условие> или же <утверждение>).
Например, рекурсивную функцию факториала можно определить так:
julia> function fact(n::Int)
n >= 0 || error("n must be non-negative")
n == 0 && return 1
n * fact(n-1)
end
fact (generic function with 1 method)
julia> fact(5)
120
julia> fact(0)
1
julia> fact(-1)
ERROR: n must be non-negative
in fact(::Int64) at ./none:2
...
Булевы операции без короткого замыкания можно выполнить с использованием битовых булевых операторов, представленных в Математических операциях и элементарных функциях: & и |. Это обычные функции, которые поддерживают синтаксис инфиксных операторов, но всегда оценивают свои аргументы:
julia> f(1) & t(2) 1 2 false julia> t(1) | t(2) 1 2 true
Как и выражения условий, используемые в if, elseif или тернарном операторе, операнды && или || должны быть булевыми значениями (true или false). Использование значения, отличного от булева, в любом месте, кроме последнего элемента в цепочке условных операторов, является ошибкой:
julia> 1 && true ERROR: TypeError: non-boolean (Int64) used in boolean context ...
С другой стороны, в конце цепочки условных операторов можно использовать любой тип выражения. Оно будет вычислено и возвращено в зависимости от предыдущих условных операторов:
julia> true && (x = rand(2,2))
2×2 Array{Float64,2}:
0.768448 0.673959
0.940515 0.395453
julia> false && (x = rand(2,2))
false
Повторяющееся вычисление: циклы
Существуют две конструкции для многократного вычисления выражений: цикл while и цикл for. Вот пример цикла while:
julia> i = 1;
julia> while i <= 5
println(i)
i += 1
end
1
2
3
4
5
Цикл while оценивает выражение условия (i <= 5 в данном случае), и до тех пор, пока оно остается true, продолжает также вычислять тело цикла while. Если выражение условия false при первом достижении цикла while, тело никогда не вычисляется.
Цикл for упрощает запись общих идиом многократного вычисления. Поскольку подсчет вверх и вниз, как в цикле while выше, очень распространен, он может быть выражен более лаконично с помощью цикла for:
julia> for i = 1:5
println(i)
end
1
2
3
4
5
Здесь 1:5 — это объект Range, представляющий последовательность чисел 1, 2, 3, 4, 5. Цикл for проходит по этим значениям, поочередно присваивая каждое из них переменной i. Существенное различие между предыдущей формой цикла while и формой цикла for — это область видимости переменной. Если переменная i не была введена в другой области видимости, в форме цикла for она видна только внутри цикла for, а не после него. Для проверки этого вам понадобится новый экземпляр интерактивной сессии или другое имя переменной:
julia> for j = 1:5
println(j)
end
1
2
3
4
5
julia> j
ERROR: UndefVarError: j not defined
...
Подробное объяснение области видимости переменных и того, как она работает в Julia, см. в разделе Область видимости переменных.
В общем случае, конструкция цикла for может перебирать любой контейнер. В этих случаях альтернативный (но полностью эквивалентный) ключевой in или ∈ обычно используется вместо =, поскольку это делает код более читаемым:
julia> for i in [1,4,0]
println(i)
end
1
4
0
julia> for s ∈ ["foo","bar","baz"]
println(s)
end
foo
bar
baz
Различные типы итерируемых контейнеров будут представлены и обсуждены в последующих разделах руководства (см., например, Многомерные массивы).
Иногда удобно завершить повторение цикла while до того, как условие проверки станет ложным, или остановить итерацию в цикле for до достижения конца итерируемого объекта. Это можно сделать с помощью ключевого слова break:
julia> i = 1;
julia> while true
println(i)
if i >= 5
break
end
i += 1
end
1
2
3
4
5
julia> for i = 1:1000
println(i)
if i >= 5
break
end
end
1
2
3
4
5
Цикл while выше никогда не завершится сам по себе, а цикл for будет итерироваться до 1000. Оба цикла прерываются преждевременно с помощью ключевого слова break.
В других случаях удобно немедленно остановить итерацию и перейти к следующей. Ключевое слово continue выполняет эту операцию:
julia> for i = 1:10
if i % 3 != 0
continue
end
println(i)
end
3
6
9
Это несколько искусственный пример, поскольку мы могли бы получить тот же результат более четко, отрицая условие и помещая вызов println внутри блока if. В реальном использовании после continue есть больше кода, и часто вызовы continue происходят из нескольких точек.
Вложенные циклы for могут быть объединены в один внешний цикл, образуя декартово произведение своих итерируемых объектов:
julia> for i = 1:2, j = 3:4
println((i, j))
end
(1,3)
(1,4)
(2,3)
(2,4)
Оператор break внутри такого цикла завершает всю вложенную последовательность циклов, а не только внутренний.
Обработка исключений
Когда возникает непредвиденное условие, функция может не суметь вернуть разумное значение своему вызывающему объекту. В таких случаях лучше всего, чтобы исключительное условие либо завершило программу, выведя сообщение об ошибке, либо, если программист предоставил код для обработки таких исключительных ситуаций, позволило этому коду принять соответствующие действия.
Встроенные Exception
Exception генерируются, когда возникло непредвиденное условие. Все перечисленные ниже встроенные Exception прерывают нормальный поток управления.
Exception |
|---|
ArgumentError |
BoundsError |
CompositeException |
DivideError |
DomainError |
EOFError |
ErrorException |
InexactError |
InitError |
InterruptException |
InvalidStateException |
KeyError |
LoadError |
OutOfMemoryError |
ReadOnlyMemoryError |
RemoteException |
MethodError |
OverflowError |
ParseError |
SystemError |
TypeError |
UndefRefError |
UndefVarError |
UnicodeError |
Например, функция sqrt() генерирует DomainError, если она применяется к отрицательному вещественному значению:
julia> sqrt(-1) ERROR: DomainError: sqrt will only return a complex result if called with a complex argument. Try sqrt(complex(x)). in sqrt(::Int64) at ./math.jl:211 ...
Вы можете определить собственные исключения следующим образом:
julia> type MyCustomException <: Exception end
Функция throw()
Исключения можно явно создать с помощью throw(). Например, функция, определенная только для неотрицательных чисел, может быть написана для генерации throw() DomainError, если аргумент отрицателен:
julia> f(x) = x>=0 ? exp(-x) : throw(DomainError()) f (generic function with 1 method) julia> f(1) 0.36787944117144233 julia> f(-1) ERROR: DomainError: in f(::Int64) at ./none:1 ...
Обратите внимание, что DomainError без скобок не является исключением, а типом исключения. Для получения объекта Exception необходимо выполнить его вызов:
julia> typeof(DomainError()) <: Exception true julia> typeof(DomainError) <: Exception false
Кроме того, некоторые типы исключений принимают один или несколько аргументов, используемых для сообщения об ошибках:
julia> throw(UndefVarError(:x)) ERROR: UndefVarError: x not defined ...
Этот механизм легко реализуется с помощью пользовательских типов исключений, аналогично тому, как написан UndefVarError:
julia> type MyUndefVarError <: Exception
var::Symbol
end
julia> Base.showerror(io::IO, e::MyUndefVarError) = print(io, e.var, " not defined");
Ошибки
Функция error() используется для создания ErrorException, который прерывает нормальный поток управления.
Предположим, мы хотим немедленно остановить выполнение, если вычисляется квадратный корень из отрицательного числа. Для этого мы можем определить более строгую версию функции sqrt(), которая генерирует ошибку, если её аргумент отрицателен:
julia> fussy_sqrt(x) = x >= 0 ? sqrt(x) : error("negative x not allowed")
fussy_sqrt (generic function with 1 method)
julia> fussy_sqrt(2)
1.4142135623730951
julia> fussy_sqrt(-1)
ERROR: negative x not allowed
in fussy_sqrt(::Int64) at ./none:1
...
Если fussy_sqrt вызывается с отрицательным значением из другой функции, вместо продолжения выполнения вызывающей функции она возвращается немедленно, отображая сообщение об ошибке в интерактивной сессии:
julia> function verbose_fussy_sqrt(x)
println("before fussy_sqrt")
r = fussy_sqrt(x)
println("after fussy_sqrt")
return r
end
verbose_fussy_sqrt (generic function with 1 method)
julia> verbose_fussy_sqrt(2)
before fussy_sqrt
after fussy_sqrt
1.4142135623730951
julia> verbose_fussy_sqrt(-1)
before fussy_sqrt
ERROR: negative x not allowed
in fussy_sqrt at ./none:1 [inlined]
in verbose_fussy_sqrt(::Int64) at ./none:3
...
Предупреждения и информационные сообщения
Julia также предоставляет другие функции, которые записывают сообщения в стандартный вывод ошибок, но не генерируют Exception и, следовательно, не прерывают выполнение:
julia> info("Hi"); 1+1
INFO: Hi
2
julia> warn("Hi"); 1+1
WARNING: Hi
2
julia> error("Hi"); 1+1
ERROR: Hi
in error(::String) at ./error.jl:21
...
Оператор try/catch
Оператор try/catch позволяет тестировать Exception. Например, настраиваемую функцию квадратного корня можно написать, чтобы автоматически вызывать метод действительного или комплексного квадратного корня по требованию с помощью Exception:
julia> f(x) = try
sqrt(x)
catch
sqrt(complex(x, 0))
end
f (generic function with 1 method)
julia> f(1)
1.0
julia> f(-1)
0.0 + 1.0im
END_OF_DOCUMENT_MARKER Важно отметить, что в реальном коде при вычислении этой функции вместо перехвата исключения следует сравнить x с нулём. Перехват исключения значительно медленнее, чем просто сравнение и ветвление.
try/catch операторы также позволяют сохранить Exception в переменной. В этом искусственном примере следующий пример вычисляет квадратный корень второго элемента x, если x является индексируемым, в противном случае предполагает, что x — действительное число и возвращает его квадратный корень:
julia> sqrt_second(x) = try
sqrt(x[2])
catch y
if isa(y, DomainError)
sqrt(complex(x[2], 0))
elseif isa(y, BoundsError)
sqrt(x)
end
end
sqrt_second (generic function with 1 method)
julia> sqrt_second([1 4])
2.0
julia> sqrt_second([1 -4])
0.0 + 2.0im
julia> sqrt_second(9)
3.0
julia> sqrt_second(-9)
ERROR: DomainError:
in sqrt_second(::Int64) at ./none:7
...
Обратите внимание, что символ, следующий за catch, всегда интерпретируется как имя исключения, поэтому при написании try/catch выражений в одну строку требуется осторожность. Следующий код не будет работать для возврата значения x в случае ошибки:
try bad() catch x end
Вместо этого используйте точку с запятой или вставьте перевод строки после catch:
try bad() catch; x end try bad() catch x end
Оператор catch не является строго необходимым; при его отсутствии значение по умолчанию — nothing.
julia> try error() end #Returns nothing
Мощность конструкции try/catch заключается в возможности немедленно разворачивать глубоко вложенное вычисление на значительно более высокий уровень в стеке вызывающих функций. Существуют ситуации, когда ошибка не произошла, но желательно разворачивать стек и передавать значение на более высокий уровень. Julia предоставляет функции rethrow(), backtrace() и catch_backtrace() для более сложной обработки ошибок.
Блоки finally
В коде, выполняющем изменения состояния или использующем ресурсы, такие как файлы, обычно требуется очистка (например, закрытие файлов), когда выполнение кода завершено. Исключение потенциально усложняет эту задачу, поскольку оно может привести к выходу блока кода из выполнения до его нормального завершения. Ключевое слово finally позволяет выполнить некоторый код при выходе из данного блока кода независимо от того, как он вышел.
Например, вот как мы можем гарантировать, что открытый файл закрыт:
f = open("file")
try
# operate on file f
finally
close(f)
end
Когда управление покидает блок try (например, из-за return, или из-за нормального завершения), выполнится close(f). Если блок try завершается из-за исключения, исключение продолжает распространяться. Блок catch можно объединить с try и finally. В этом случае блок finally выполнится после того, как catch обработает ошибку.
Задачи (или сопрограммы)
Задачи — это элемент управления потоком, позволяющий приостанавливать и возобновлять вычисления гибким образом. Иногда этот элемент называют симметричными сопрограммами, лёгкими потоками, кооперативным многозадачным режимом или одноразовыми продолжениями.
Когда фрагмент вычислений (на практике — выполнение определённой функции) определяется как Task, становится возможным прервать его, переключившись на другую Task. Исходная Task позже может быть возобновлена, и в этом случае она продолжит выполнение с того места, где остановилась. Сначала это может показаться похожим на вызов функции. Однако есть два ключевых отличия. Во-первых, переключение задач не использует память, поэтому любое количество переключений задач может происходить без потребления стека вызовов. Во-вторых, переключение между задачами может происходить в любом порядке, в отличие от вызовов функций, где вызываемая функция должна завершить выполнение, прежде чем управление вернётся к вызывающей функции.
Этот вид управления потоком может значительно облегчить решение определённых проблем. В некоторых задачах различные фрагменты необходимой работы не связаны естественным образом вызовами функций; среди задач, которые нужно выполнить, нет очевидного «вызывающего» или «вызываемого». Примером является проблема «производитель-потребитель», где одна сложная процедура генерирует значения, а другая сложная процедура их потребляет. Потребитель не может просто вызвать функцию производителя, чтобы получить значение, потому что производитель может иметь больше значений для генерации, и поэтому может быть ещё не готов вернуть значение. С помощью задач производитель и потребитель могут работать столько, сколько нужно, передавая значения друг другу по мере необходимости.
Julia предоставляет функции produce() и consume() для решения этой проблемы. Производитель — это функция, которая вызывает produce() для каждого значения, которое нужно произвести:
julia> function producer()
produce("start")
for n=1:4
produce(2n)
end
produce("stop")
end;
Для потребления значений сначала производитель оборачивается в Task, затем consume() вызывается многократно на этом объекте:
julia> p = Task(producer); julia> consume(p) "start" julia> consume(p) 2 julia> consume(p) 4 julia> consume(p) 6 julia> consume(p) 8 julia> consume(p) "stop"
Один из способов интерпретировать это поведение — то, что producer смог вернуться несколько раз. Между вызовами produce() выполнение производителя приостанавливается, и управление передаётся потребителю.
Задача может быть использована как итерируемый объект в цикле for, в этом случае переменная цикла принимает все произведённые значения:
julia> for x in Task(producer)
println(x)
end
start
2
4
6
8
stop
Обратите внимание, что конструктор Task() ожидает функцию без аргументов. Распространённый шаблон — параметризованный производитель, в этом случае требуется частичное применение функции для создания функции без аргументов анонимной функции. Это можно сделать либо напрямую, либо с помощью удобной макрокоманды:
function mytask(myarg)
...
end
taskHdl = Task(() -> mytask(7))
# or, equivalently
taskHdl = @task mytask(7)
produce() и consume() не запускают потоки, которые могут выполняться на отдельных процессорах. Истинные потоки ядра рассматриваются в разделе Параллельное вычисление.
Основные операции с задачами
Хотя produce() и consume() иллюстрируют суть задач, они фактически реализованы как функции библиотеки с использованием более примитивной функции yieldto(). yieldto(task,value) приостанавливает текущую задачу, переключается на указанную task, и заставляет последний вызов yieldto() этой задачи вернуть указанное значение value. Обратите внимание, что yieldto() — единственная операция, необходимая для использования управления потоком в стиле задач; вместо вызова и возврата мы всегда просто переключаемся на другую задачу. Именно поэтому этот элемент также называют «симметричными сопрограммами»; к каждой задаче осуществляется доступ и отключение с помощью одного и того же механизма.
yieldto() мощная, но большинство использований задач не вызывают её напрямую. Подумайте, почему это может быть так. Если вы переключаетесь с текущей задачи, вы, вероятно, захотите вернуться к ней в какой-то момент, но знание того, когда переключаться обратно, и знание того, какая задача несёт ответственность за переключение обратно, может потребовать значительной координации. Например, produce() должна поддерживать некоторое состояние, чтобы запомнить, кто является потребителем. То, что не нужно вручную отслеживать задачу потребления, упрощает использование produce() по сравнению с yieldto().
В дополнение к yieldto(), для эффективного использования задач требуется несколько других основных функций.
-
current_task()получает ссылку на текущую задачу. -
istaskdone()проверяет, завершена ли задача. -
istaskstarted()проверяет, выполнена ли задача. -
task_local_storage()управляет хранилищем ключевых значений, специфичным для текущей задачи.
Задачи и события
Большинство переключений задач происходят в результате ожидания событий, таких как запросы ввода-вывода, и выполняются планировщиком, включённым в стандартную библиотеку. Планировщик поддерживает очередь исполняемых задач и выполняет цикл обработки событий, который перезапускает задачи на основе внешних событий, таких как приход сообщений.
Основная функция ожидания события — wait(). Несколько объектов реализуют wait(); например, для объекта Process, wait() будет ожидать его завершения. wait() часто неявная; например, вызов read() может подразумевать ожидание готовности данных.
Во всех этих случаях wait() в конечном счёте работает с объектом Condition, который отвечает за очереди и перезапуск задач. Когда задача вызывает wait() на Condition, задача помечается как неподготовленная к выполнению, добавляется в очередь условия и переключается на планировщик. Затем планировщик выберет другую задачу для выполнения или заблокируется, ожидая внешних событий. Если всё пройдёт успешно, в конечном итоге обработчик события вызовет notify() на условии, что позволит задачам, ожидающим этого условия, снова стать готовыми к выполнению.
Задача, созданная явно с помощью вызова Task, изначально не известна планировщику. Это позволяет вам вручную управлять задачами с помощью yieldto(), если это необходимо. Однако, когда такая задача ожидает события, она всё равно автоматически перезапускается при его появлении, как и ожидалось. Также можно заставить планировщик выполнять задачу всякий раз, когда это возможно, без необходимости ожидания каких-либо событий. Это делается с помощью вызова schedule() или с помощью макросов @schedule или @async (подробнее см. Параллельное программирование).
Состояния задач
Задачи имеют поле state, описывающее их статус выполнения. Состояние задачи — это один из следующих символов:
| Символ | Значение |
|---|---|
:runnable | В настоящее время выполняется или доступна для переключения |
:waiting | Заблокирована, ожидая определённого события |
:queued | В очереди планировщика, готовая к перезапуску |
:done | Успешно завершена |
:failed | Завершена с неперехваченным исключением |
© 2009–2016 Jeff Bezanson, Stefan Karpinski, Viral B. Shah, and other contributors
Licensed under the MIT License.
https://docs.julialang.org/en/release-0.5/manual/control-flow/