Spec-Zone.ru › Julia 1.4

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

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 прерывают нормальный поток управления.

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

Например, функция 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 часто является неявным; например, ожидание доступности данных в вызове read может происходить внутри wait.

Во всех этих случаях 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.4.2/manual/control-flow/

Spec-Zone.ru

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