Spec-Zone.ru › Julia 0.6

Поток управления

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.

Самый простой способ понять это поведение — посмотреть на пример. В предыдущем примере вызов 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] fact(::Int64) at ./none:2

Булевы операции без короткого замыкания можно выполнять с помощью битовых булевых операторов, представленных в Математических операциях и элементарных функциях: & и |. Это обычные функции, которые поддерживают синтаксис инфиксных операторов, но всегда вычисляют свои аргументы:

julia> f(1) & t(2)
1
2
false

julia> t(1) | t(2)
1
2
true

Точно так же, как выражения условия, используемые в if, elseif или тернарном операторе, операнды && или || должны быть булевыми значениями (true или false). Использование небулевого значения в любом месте, кроме последнего элемента в цепочке условных выражений, является ошибкой:

julia> 1 && true
ERROR: TypeError: non-boolean (Int64) used in boolean context

С другой стороны, любой тип выражения может использоваться в конце цепочки условных выражений. Он будет вычислен и возвращён в зависимости от предыдущих условных выражений:

julia> true && (x = (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)
           i += 1
       end
1
2
3
4
5

Цикл while вычисляет выражение условия (в данном случае i <= 5) и, пока оно остаётся true, продолжает вычислять тело цикла while. Если выражение условия имеет значение false при первом достижении цикла while, тело цикла никогда не вычисляется.

Цикл for упрощает написание общих идиом многократного вычисления. Поскольку подсчёт вверх и вниз, как в цикле while выше, очень распространён, его можно записать более лаконично с помощью цикла for:

julia> for i = 1:5
           println(i)
       end
1
2
3
4
5

Здесь 1:5 — это объект Range, представляющий последовательность чисел 1, 2, 3, 4, 5. Цикл for проходит по этим значениям, поочерёдно присваивая каждое из них переменной i. Существенное различие между предыдущей формой цикла while и формой цикла for заключается в области видимости переменной. Если переменная i не была объявлена в другой области видимости, в форме цикла for, она видна только внутри цикла for, а не после него. Для проверки этого необходимо либо создать новый экземпляр интерактивной сессии, либо использовать другое имя переменной:

julia> for j = 1:5
           println(j)
       end
1
2
3
4
5

julia> j
ERROR: UndefVarError: j not defined

Подробное объяснение области видимости переменных и её работы в Julia см. в разделе Область видимости переменных.

В общем случае, конструкция цикла for может итерироваться по любому контейнеру. В таких случаях, альтернативный (но полностью эквивалентный) ключевой запрос in или ∈ обычно используется вместо =, так как это делает код более читаемым:

julia> for i in [1,4,0]
           println(i)
       end
1
4
0

julia> for s ∈ ["foo","bar","baz"]
           println(s)
       end
foo
bar
baz

Различные типы итерируемых контейнеров будут введены и обсуждены в последующих разделах руководства (см., например, Многомерные массивы).

Иногда удобно завершить повторение цикла while до того, как условие проверки станет ложным, или остановить итерацию в цикле for до достижения конца итерируемого объекта. Это можно сделать с помощью ключевого слова break.

julia> i = 1;

julia> while true
           println(i)
           if i >= 5
               break
           end
           i += 1
       end
1
2
3
4
5

julia> for i = 1:1000
           println(i)
           if i >= 5
               break
           end
       end
1
2
3
4
5

Без ключевого слова 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)

Оператор break внутри такого цикла завершает выполнение всего вложенного цикла, а не только внутреннего.

Обработка исключений

Когда возникает непредвиденная ситуация, функция может не иметь возможности вернуть разумное значение своему вызывающему объекту. В таких случаях предпочтительно, чтобы исключительное условие либо завершило выполнение программы, выведя диагностическое сообщение об ошибке, либо, если программист предоставил код для обработки таких исключительных обстоятельств, позволило этому коду выполнить соответствующее действие.

Встроенные Exception

Exception выбрасываются, когда возникла непредвиденная ситуация. Все перечисленные ниже встроенные Exception прерывают нормальный поток управления.

Exception
ArgumentError
BoundsError
CompositeException
DivideError
DomainError
EOFError
ErrorException
InexactError
InitError
InterruptException
InvalidStateException
KeyError
LoadError
OutOfMemoryError
ReadOnlyMemoryError
RemoteException
MethodError
OverflowError
ParseError
SystemError
TypeError
UndefRefError
UndefVarError
UnicodeError

Например, функция sqrt() выбрасывает DomainError, если применяется к отрицательному действительному значению:

julia> sqrt(-1)
ERROR: DomainError:
sqrt will only return a complex result if called with a complex argument. Try sqrt(complex(x)).
Stacktrace:
 [1] sqrt(::Int64) at ./math.jl:434

Вы можете определять собственные исключения следующим образом:

julia> struct MyCustomException <: Exception end

Функция throw()

Исключения можно создавать явно с помощью throw(). Например, функцию, определённую только для неотрицательных чисел, можно переписать для выбрасывания throw() DomainError, если аргумент отрицательный:

julia> f(x) = x>=0 ? exp(-x) : throw(DomainError())
f (generic function with 1 method)

julia> f(1)
0.36787944117144233

julia> f(-1)
ERROR: DomainError:
Stacktrace:
 [1] f(::Int64) at ./none:1

Обратите внимание, что DomainError без скобок не является исключением, а типом исключения. Для получения объекта Exception необходимо вызвать его:

julia> typeof(DomainError()) <: Exception
true

julia> typeof(DomainError) <: Exception
false

Кроме того, некоторые типы исключений принимают один или несколько аргументов, используемых для отчёта об ошибках:

julia> throw(UndefVarError(:x))
ERROR: UndefVarError: x not defined

Этот механизм легко реализуется с помощью пользовательских типов исключений, аналогично тому, как написан UndefVarError:

julia> 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] fussy_sqrt(::Int64) at ./none:1

Если fussy_sqrt вызывается с отрицательным значением из другой функции, вместо продолжения выполнения вызывающей функции, она немедленно возвращается, выводит сообщение об ошибке в интерактивной сессии:

julia> function verbose_fussy_sqrt(x)
           println("before fussy_sqrt")
           r = fussy_sqrt(x)
           println("after fussy_sqrt")
           return r
       end
verbose_fussy_sqrt (generic function with 1 method)

julia> verbose_fussy_sqrt(2)
before fussy_sqrt
after fussy_sqrt
1.4142135623730951

julia> verbose_fussy_sqrt(-1)
before fussy_sqrt
ERROR: negative x not allowed
Stacktrace:
 [1] fussy_sqrt at ./none:1 [inlined]
 [2] verbose_fussy_sqrt(::Int64) at ./none:3

Предупреждения и информационные сообщения

Julia также предоставляет другие функции, которые записывают сообщения в стандартный поток ошибок, но не выбрасывают никаких Exception и, следовательно, не прерывают выполнение:

julia> info("Hi"); 1+1
INFO: Hi
2

julia> warn("Hi"); 1+1
WARNING: Hi
2

julia> error("Hi"); 1+1
ERROR: Hi
Stacktrace:
 [1] error(::String) at ./error.jl:21

Оператор try/catch

Оператор try/catch позволяет проверять возникновение Exception. Например, можно написать настраиваемую функцию квадратного корня, которая автоматически использует метод действительного или комплексного квадратного корня по требованию, используя Exception:

julia> f(x) = try
           sqrt(x)
       catch
           sqrt(complex(x, 0))
       end
f (generic function with 1 method)

julia> f(1)
1.0

julia> f(-1)
0.0 + 1.0im

Важно отметить, что в реальном коде вычисления этой функции нужно сравнивать 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:
Stacktrace:
 [1] sqrt_second(::Int64) at ./none:7

Обратите внимание, что символ, следующий за catch, всегда интерпретируется как имя исключения, поэтому при написании выражений try/catch на одной строке нужна осторожность. Следующий код не будет работать для возврата значения x в случае ошибки:

try bad() catch x end

Вместо этого используйте точку с запятой или перенос строки после catch:

try bad() catch; x end

try bad()
catch
    x
end

Оператор catch не является строго необходимым; при его отсутствии значение по умолчанию — nothing.

julia> try error() end # Returns nothing

Мощность конструкции try/catch заключается в возможности немедленно развернуть глубоко вложенное вычисление на гораздо более высокий уровень в стеке вызывающих функций. Существуют ситуации, когда ошибка не произошла, но желательно развернуть стек и передать значение на более высокий уровень. Julia предоставляет функции rethrow(), backtrace() и catch_backtrace() для более продвинутой обработки ошибок.

finally блоки

В коде, выполняющем изменения состояния или использующем ресурсы, такие как файлы, обычно есть работа по очистке (например, закрытие файлов), которую необходимо выполнить, когда код завершён. Исключение потенциально усложняет эту задачу, так как оно может привести к выходу из блока кода до достижения его нормального конца. Ключевое слово finally предоставляет способ выполнения некоторого кода при выходе из данного блока кода, независимо от того, как этот выход произошёл.

Например, вот как мы можем гарантировать, что открытый файл закрывается:

f = open("file")
try
    # operate on file f
finally
    close(f)
end

Когда управление покидает блок try (например, из-за return, или просто завершив работу нормально), close(f) будет выполнено. Если блок try завершается из-за исключения, исключение продолжит распространяться. Блок catch может быть объединен с try и finally. В этом случае блок finally будет выполнен после того, как catch обработает ошибку.

Задачи (также известные как сопрограммы)

Задачи — это элемент управления потоком, который позволяет приостанавливать и возобновлять вычисления гибким образом. Этот элемент иногда называют другими именами, такими как симметричные сопрограммы, лёгкие потоки, кооперативная многозадачность или одноразовые продолжения.

Когда часть вычислительной работы (на практике, выполнение конкретной функции) назначена как Task, становится возможным прерывать её, переключаясь на другую Task. Исходную Task можно позже возобновить, и в этот момент она продолжит выполнение с того места, где остановилась. На первый взгляд, это может показаться похожим на вызов функции. Однако есть два ключевых различия. Во-первых, переключение задач не использует память, поэтому любое количество переключений задач может произойти без потребления стека вызовов. Во-вторых, переключение между задачами может происходить в любом порядке, в отличие от вызовов функций, где вызываемая функция должна закончить выполнение, прежде чем управление вернётся к вызывающей функции.

Этот тип управления потоком может значительно упростить решение определённых задач. В некоторых задачах различные части необходимой работы не связаны естественным образом вызовами функций; нет очевидного «отправителя» или «получателя» среди задач, которые нужно выполнить. Примером является задача «производитель-потребитель», где одна сложная процедура генерирует значения, а другая сложная процедура их потребляет. Потребитель не может просто вызвать функцию производителя, чтобы получить значение, потому что производитель может иметь больше значений для генерации и, следовательно, может ещё не быть готовым вернуть значение. С помощью задач производитель и потребитель могут оба работать столько, сколько им нужно, передавая значения друг другу по мере необходимости.

Julia предоставляет механизм 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() управляет хранилищем пар «ключ-значение», специфичным для текущей задачи.

Задачи и события

Большинство переключений задач происходит в результате ожидания событий, таких как запросы ввода-вывода, и выполняются планировщиком, включённым в стандартную библиотеку. Планировщик поддерживает очередь готовых задач и выполняет цикл событий, который перезапускает задачи на основе внешних событий, таких как прибытие сообщений.

Основной функцией ожидания события является wait(). Несколько объектов реализуют wait(); например, для объекта Process, wait() будет ждать его завершения. wait() часто подразумевается; например, wait() может происходить внутри вызова read() для ожидания доступности данных.

Во всех этих случаях wait() в конечном итоге работает с объектом Condition, который отвечает за очереди и перезапуск задач. Когда задача вызывает wait() для Condition, задача помечается как не готовая к выполнению, добавляется в очередь условия и переключается на планировщик. Планировщик затем выбирает другую задачу для выполнения или блокируется, ожидая внешних событий. При успешной работе, в конечном итоге обработчик событий вызовет notify() для условия, что заставляет задачи, ожидающие этого условия, стать готовыми к выполнению.

Задача, созданная явно с помощью вызова Task, поначалу не известна планировщику. Это позволяет вам вручную управлять задачами с помощью yieldto(), если вам это нужно. Однако, когда такая задача ожидает события, она по-прежнему автоматически перезапускается, когда событие происходит, как и ожидалось. Также возможно заставить планировщик выполнять задачу всякий раз, когда это возможно, без необходимости ожидания каких-либо событий. Это делается путём вызова schedule() или с помощью макросов @schedule или @async (см. Параллельное программирование для получения более подробной информации).

Состояния задач

Задачи имеют поле state, которое описывает состояние их выполнения. Состояние задачи Task state — это один из следующих символов:

Символ Значение
:runnable В данный момент выполняется или доступен для переключения
:waiting Заблокирован в ожидании определенного события
:queued В очереди планировщика, готов к перезапуску
:done Успешно завершено выполнение
:failed Завершено с неперехваченным исключением

© 2009–2016 Jeff Bezanson, Stefan Karpinski, Viral B. Shah, and other contributors
Licensed under the MIT License.
https://docs.julialang.org/en/release-0.6/manual/control-flow/

Spec-Zone.ru

Настройки Оффлайн Что нового Помощь О нас
Spec-Zone .ru
спецификации, руководства, описания, API