Поток управления
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 и Base.catch_stack для более продвинутой обработки ошибок.
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, принимающий функцию с одним аргументом в качестве аргумента, чтобы запустить задачу, связанную с каналом. Затем мы можем 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.1.1/manual/control-flow/