Поток управления
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 «протекающие», т.е. они не вводят локальную область видимости. Это означает, что новые переменные, определенные внутри if могут быть использованы после блока 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)
julia> test(2, 1)
x is greater than y.
Переменная 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
Stacktrace:
[1] 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. Обратите внимание, что пробелы вокруг ? и : обязательны: выражение вроде a?b:c не является допустимым тернарным выражением (но перенос строки допустим после ? и :).
Самый простой способ понять это поведение — посмотреть на пример. В предыдущем примере вызов 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
Stacktrace:
[1] error at ./error.jl:33 [inlined]
[2] fact(::Int64) at ./none:2
[3] top-level scope
Булевы операции без короткого замыкания могут быть выполнены с помощью битовых булевых операторов, представленных в Математические операции и элементарные функции: & и |. Это обычные функции, которые, как оказалось, поддерживают синтаксис инфиксных операторов, но всегда вычисляют свои аргументы:
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 = (1, 2, 3)) (1, 2, 3) julia> false && (x = (1, 2, 3)) false
Повторяющееся вычисление: циклы
Существуют две конструкции для повторного вычисления выражений: цикл while и цикл for. Вот пример цикла while:
julia> i = 1;
julia> while i <= 5
println(i)
global 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 — это объект диапазона, представляющий последовательность чисел 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
global i += 1
end
1
2
3
4
5
julia> for j = 1:1000
println(j)
if j >= 5
break
end
end
1
2
3
4
5
Без ключевого слова break вышеуказанный цикл 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)
С помощью этого синтаксиса итерируемые объекты могут по-прежнему ссылаться на переменные внешнего цикла; например, for i = 1:n, j = 1:i допустимо. Однако оператор break внутри такого цикла выходит из всего набора циклов, а не только из внутреннего. Обе переменные (i и j ) каждый раз устанавливаются в текущие значения итерации при каждом выполнении внутреннего цикла. Таким образом, присваивания i не будут видны последующим итерациям:
julia> for i = 1:2, j = 3:4
println((i, j))
i = 0
end
(1, 3)
(1, 4)
(2, 3)
(2, 4)
Если бы этот пример был переписан с использованием ключевого слова for для каждой переменной, то вывод был бы другим: второе и четвёртое значения содержали бы 0.
Обработка исключений
При возникновении непредвиденных условий функция может не смочь вернуть разумное значение своему вызывающему объекту. В таких случаях лучше всего, чтобы исключительное условие либо завершило программу с сообщением об ошибке диагностики, либо, если программист предоставил код для обработки таких исключительных обстоятельств, позволить этому коду принять соответствующие меры.
Встроенные Exception
Exception выбрасываются, когда произошла непредвиденная ситуация. Перечисленные ниже встроенные Exception прерывают нормальный поток управления.
Например, функция sqrt выбрасывает DomainError, если её применить к отрицательному действительному значению:
julia> sqrt(-1) ERROR: DomainError with -1.0: sqrt will only return a complex result if called with a complex argument. Try sqrt(Complex(x)). Stacktrace: [...]
Вы можете определить собственные исключения следующим образом:
julia> struct MyCustomException <: Exception end
Функция throw
Исключения можно создавать явно с помощью throw. Например, функция, определённая только для неотрицательных чисел, может быть написана так, чтобы throw DomainError, если аргумент отрицательный:
julia> f(x) = x>=0 ? exp(-x) : throw(DomainError(x, "argument must be nonnegative")) f (generic function with 1 method) julia> f(1) 0.36787944117144233 julia> f(-1) ERROR: DomainError with -1: argument must be nonnegative Stacktrace: [1] f(::Int64) at ./none:1
Обратите внимание, что DomainError без скобок не является исключением, а является типом исключения. Для получения объекта Exception его необходимо вызвать:
julia> typeof(DomainError(nothing)) <: Exception true julia> typeof(DomainError) <: Exception false
Кроме того, некоторые типы исключений принимают один или несколько аргументов, которые используются для отчёта об ошибках:
julia> throw(UndefVarError(:x)) ERROR: UndefVarError: x not defined
Этот механизм можно легко реализовать с помощью пользовательских типов исключений, следуя тому, как написан UndefVarError:
julia> struct MyUndefVarError <: Exception
var::Symbol
end
julia> Base.showerror(io::IO, e::MyUndefVarError) = print(io, e.var, " not defined")
При написании сообщения об ошибке предпочтительно использовать первую букву в нижнем регистре. Например, size(A) == size(B) || throw(DimensionMismatch("size of A not equal to size of B"))
предпочтительнее, чем
size(A) == size(B) || throw(DimensionMismatch("Size of A not equal to size of B")).
Однако иногда имеет смысл сохранить заглавную первую букву, например, если аргумент функции — это заглавная буква: size(A,1) == size(B,2) || throw(DimensionMismatch("A has first dimension...")).
Ошибки
Функция 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
Stacktrace:
[1] error at ./error.jl:33 [inlined]
[2] fussy_sqrt(::Int64) at ./none:1
[3] top-level scope
Если 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
Stacktrace:
[1] error at ./error.jl:33 [inlined]
[2] fussy_sqrt at ./none:1 [inlined]
[3] verbose_fussy_sqrt(::Int64) at ./none:3
[4] top-level scope
Оператор 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
Важно отметить, что в реальном коде, вычисляющем эту функцию, нужно было бы сравнивать 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 with -9.0:
sqrt will only return a complex result if called with a complex argument. Try sqrt(Complex(x)).
Stacktrace:
[...]
Обратите внимание, что символ, следующий за catch, всегда будет интерпретироваться как имя исключения, поэтому при написании выражений try/catch в одной строке нужно быть осторожным. Следующий код не будет работать, чтобы вернуть значение x в случае ошибки:
try bad() catch x end
Вместо этого используйте точку с запятой или добавьте перевод строки после catch:
try bad() catch; x end
try bad()
catch
x
end
Мощность конструкции 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 предоставляет механизм Channel для решения этой проблемы. Channel — это очередь FIFO, которая может иметь несколько задач, читающих и записывающих в неё.
Определим задачу производителя, которая производит значения через вызов put!. Чтобы потреблять значения, нам нужно запланировать выполнение производителя в новой задаче. Можно использовать специальный конструктор Channel, который принимает функцию с 1 аргументом, для запуска задачи, привязанной к каналу. Затем мы можем повторять take! значения из объекта канала:
julia> function producer(c::Channel)
put!(c, "start")
for n=1:4
put!(c, 2n)
end
put!(c, "stop")
end;
julia> chnl = Channel(producer);
julia> take!(chnl)
"start"
julia> take!(chnl)
2
julia> take!(chnl)
4
julia> take!(chnl)
6
julia> take!(chnl)
8
julia> take!(chnl)
"stop"
Один из способов понять это поведение заключается в том, что producer смог вернуть несколько раз. Между вызовами put!, выполнение производителя приостанавливается, и управление получает потребитель.
Возвращаемый Channel может использоваться как итерируемый объект в цикле for, в этом случае переменная цикла принимает все произведённые значения. Цикл завершается, когда канал закрывается.
julia> for x in Channel(producer)
println(x)
end
start
2
4
6
8
stop
Обратите внимание, что нам не нужно было явно закрывать канал в производителе. Это связано с тем, что привязка Channel к Task связывает открытый жизненный цикл канала с жизненным циклом привязанной задачи. Объект канала закрывается автоматически при завершении задачи. К одной задаче можно привязать несколько каналов, и наоборот.
Хотя конструктор Task ожидает функцию с 0 аргументами, метод Channel, создающий задачу, привязанную к каналу, ожидает функцию, принимающую один аргумент типа Channel. Распространённым шаблоном является параметризация производителя, в этом случае требуется частичное применение функции для создания функции с 0 или 1 аргументом анонимной функции.
Для объектов Task это можно сделать либо напрямую, либо с помощью удобного макроса:
function mytask(myarg)
...
end
taskHdl = Task(() -> mytask(7))
# or, equivalently
taskHdl = @task mytask(7)
Для организации более сложных шаблонов распределения работы bind и schedule могут использоваться совместно с Task и Channel конструкторами для явного связывания набора каналов с набором задач производителя/потребителя.
Обратите внимание, что в настоящее время задачи Julia не планируются на выполнение на отдельных ядрах процессора. Истинные потоки ядра обсуждаются в разделе Параллельное вычисление.
Основные операции с задачами
Давайте рассмотрим низкоуровневую конструкцию yieldto, чтобы понять, как работает переключение задач. yieldto(task,value) приостанавливает текущую задачу, переключается на указанную task, и заставляет последний вызов yieldto этой задачи вернуть указанное value. Обратите внимание, что yieldto — единственная операция, необходимая для использования управления потоком на основе задач; вместо вызова и возврата мы всегда просто переключаемся на другую задачу. Именно поэтому эта функция также называется «симметричными корутинами»; каждая задача переключается на неё и с неё с помощью одного и того же механизма.
yieldto мощная, но большинство приложений задач не вызывают её напрямую. Подумайте, почему это может быть так. Если вы переключаетесь с текущей задачи, вы, вероятно, захотите вернуться к ней в какой-то момент, но знание того, когда вернуться, и знание того, какая задача несет ответственность за возврат, может потребовать значительной координации. Например, put! и take! являются блокирующими операциями, которые, когда используются в контексте каналов, поддерживают состояние для запоминания потребителей. Необходимость вручную отслеживать потребительскую задачу облегчает использование put!, чем низкоуровневый yieldto.
Помимо yieldto, для эффективного использования задач необходимы и некоторые другие базовые функции.
-
current_taskполучает ссылку на текущую выполняющуюся задачу. -
istaskdoneпроверяет, завершилась ли задача. -
istaskstartedпроверяет, выполнена ли задача. -
task_local_storageобрабатывает хранилище ключей-значений, специфичное для текущей задачи.
Задачи и события
Большинство переключений задач происходит в результате ожидания событий, таких как запросы ввода-вывода, и выполняется планировщиком, включенным в Julia Base. Планировщик поддерживает очередь исполняемых задач и выполняет цикл обработки событий, который перезапускает задачи на основе внешних событий, таких как приход сообщений.
Основной функцией для ожидания события является wait. Несколько объектов реализуют wait; например, учитывая объект Process, wait будет ждать его завершения. wait часто является неявной; например, wait может произойти внутри вызова read для ожидания доступности данных.
Во всех этих случаях wait в конечном счёте работает с объектом Condition, который отвечает за планирование и перезапуск задач. Когда задача вызывает wait на Condition, задача помечается как неисполняемая, добавляется в очередь условия и переключается на планировщик. Планировщик затем выберет другую задачу для выполнения или заблокируется в ожидании внешних событий. При благоприятном исходе, в конечном итоге обработчик событий вызовет notify на условии, что приводит к тому, что ожидающие этого условия задачи снова станут исполняемыми.
Задача, созданная явно путём вызова Task, изначально неизвестна планировщику. Это позволяет вам управлять задачами вручную с помощью yieldto, если вам это нужно. Однако, когда такая задача ждёт события, она всё равно перезапускается автоматически при его наступлении, как вы и ожидаете. Также можно заставить планировщик запускать задачу, когда это возможно, не обязательно ожидая каких-либо событий. Это делается путём вызова schedule или использования макроса @async (см. Параллельное вычисление для получения более подробной информации).
Состояния задач
Задачи имеют поле state, которое описывает статус их выполнения. Состояние Task state является одним из следующих символов:
| Символ | Значение |
|---|---|
:runnable |
В настоящее время выполняется или доступно для переключения |
:waiting |
Заблокировано в ожидании определённого события |
:queued |
В очереди планировщика, готовится к перезапуску |
:done |
Успешно завершено выполнение |
:failed |
Завершено с непредвиденной ошибкой |
© 2009–2019 Jeff Bezanson, Stefan Karpinski, Viral B. Shah, and other contributors
Licensed under the MIT License.
https://docs.julialang.org/en/v1.0.4/manual/control-flow/