Spec-Zone.ru › Julia 0.7

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

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

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

julia> f(1)
1.0

julia> f(-1)
0.0 + 1.0im

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

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

julia> sqrt_second(x) = try
           sqrt(x[2])
       catch y
           if isa(y, DomainError)
               sqrt(complex(x[2], 0))
           elseif isa(y, BoundsError)
               sqrt(x)
           end
       end
sqrt_second (generic function with 1 method)

julia> sqrt_second([1 4])
2.0

julia> sqrt_second([1 -4])
0.0 + 2.0im

julia> sqrt_second(9)
3.0

julia> sqrt_second(-9)
ERROR: DomainError with -9.0:
sqrt will only return a complex result if called with a complex argument. Try sqrt(Complex(x)).
Stacktrace:
[...]

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

try bad() catch x end

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

try bad() catch; x end

try bad()
catch
    x
end

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

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 В настоящее время выполняется или доступно для переключения
:waiting Заблокировано в ожидании конкретного события
:queued В очереди задач планировщика, готовится к перезапуску
:done Успешно завершено выполнение
:failed Завершено с необработанным исключением

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

Spec-Zone.ru

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