Поток управления
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 ожидает функцию с 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 |
В настоящее время выполняется или может быть выполнена |
:done |
Успешно завершено выполнение |
:failed |
Завершено с неперехваченным исключением |
© 2009–2020 Jeff Bezanson, Stefan Karpinski, Viral B. Shah, and other contributors
Licensed under the MIT License.
https://docs.julialang.org/en/v1.3.1/manual/control-flow/