Поток управления
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 и обрабатывать ситуации, которые могут привести к сбою вашего приложения. Например, в приведенном ниже коде функция для извлечения квадратного корня обычно выбрасывает исключение. Поместив блок try/catch вокруг него, мы можем здесь избежать этого. Вы можете выбрать, как вы хотите обработать это исключение, будь то его регистрация, возврат значения-заполнителя или, как в данном случае, просто вывод сообщения. При принятии решения о способе обработки непредвиденных ситуаций следует учитывать, что использование блока try/catch значительно медленнее, чем использование условных проверок для обработки этих ситуаций. Ниже приведены дополнительные примеры обработки исключений с помощью блока try/catch:
julia> try
sqrt("ten")
catch e
println("You should have entered a numeric value")
end
You should have entered a numeric value
Операторы 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, который принимает функцию с 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 ожидает функцию без аргументов, метод 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 |
В настоящее время выполняется или может быть выполнена |
: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.2.0/manual/control-flow/