制御フロー
Juliaには様々な制御フローがあります。
- 複合式:
beginと(;) - 条件評価:
if-elseif-elseと?:(三項演算子) - 短絡評価:
&&,||と 比較の連鎖 - 反復評価:ループ:
whileとfor. - 例外処理:
try-catch,errorとthrow. - タスク (別名コルーチン):
yieldto.
最初の5つの制御フローのしくみは高水準のプログラム言語に標準的なものです。 タスクはそれほど標準的ではありませんが、非ローカルの制御フローで一時的に中断した計算を切り替える事ができます。 これは強力で、例外処理や協調的マルチタスクは、Juliaでは、タスクを使って実装されています。 日々のプログラムでタスクを直接使うわけではないですが、ある種のプログラムの問題ではタスクを使うと簡単に解決できます。
複合式
単一の式で、何個かの部分式を順に評価し、最後の部分式の値をその式の値として返す、といったことができると便利なことが時々あります。 Juliaにはこれを達成する2つの構文があります。beginブロックとセミコロン(;)連鎖です。 共に複合式の値は、最後の部分式の値です。 ここにbeginブロックの例を挙げます。
julia> z = begin
x = 1
y = 2
x + y
end
3これはかなり小さな単一式なので、簡単にセミコロン(;)連鎖の構文を使って一行にまとめることができます。
julia> z = (x = 1; y = 2; x + y)
3この構文は、特に関数で紹介した、簡潔な1行での関数の定義に役立ちます。 beginブロックは複数行で、セミコロン(;)連鎖は1行で使うのが普通ですが、必ずしもこれに従う必要はありません。
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 yelseifとelseのブロックは省略可能で、elseifブロックは好きな数だけ使うことができます。 if-elseif-else構文の条件式は、初めてtrueに評価されるものが出てくるまで続き、あとはその真の条件式に対応するブロックが評価され、 さらに条件式やブロックが評価されることはありません。
ifブロックには、"漏れ"があります。 つまり、ローカルスコープを採用していません。 これは、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!"とても短い(1行の)条件文は、Juliaではよく短絡評価を使って表現される点に注意してください。 これは、次のセクションで概説します。
C, MATLAB, Perl, Python, Rubyなどとは異なり、しかしJavaやその他少数の型付き言語と同様に、 条件式の値が trueやfalse以外の場合は、エラーになります。
julia> if 1
println("true")
end
ERROR: TypeError: non-boolean (Int64) used in boolean contextこのエラーは、条件式の値の型が、求められるBoolではなく不当なInt64であることを示します。
いわゆる"三項演算子"の?:はif-elseif-else構文にとても近いですが、 条件式の選択が単一式の値からだけの場合に限られます。 長いブロックのコードを持つ条件文を実行する場合は使えません。 この名前の由来は、多くの言語で被演算子が3個の唯一の演算子だからです。
a ? b : c?の前の式aは条件式で、aがtrueの時は、:の前の式bを評価し、aがfalseの時は式cを評価します。 ?や:の周りの空白は必須である点に注意してください。 a?b:cのように書いた式は、無効な三項演算子です。 (しかし?や:のあとに改行を入れるのは構いません)
この挙動を理解する一番簡単な方法は、例をみることです。 前述の例では、printlnの呼び出しは3つの分岐で共有しています。 実際に選択しているのは、印字する文字列リテラルです。 これは、三項演算子を使ってもっと簡潔に書くことができます。 もっとはっきりさせるために、先に2つの選択の場合をやってみましょう。
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"に 三項演算子全体が評価されます。 もともとの3選択の例には、三項演算子を複数連鎖させる必要があります。
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はaがfalseの時はbの値にかかわらず必ずfalseになり、同様に a && bはaがtureの時はbの値にかかわらず必ずtrueからです。 &&と||は両方とも右結合ですが、&&のほうが ||より優先順位が高いです。 この挙動は簡単に実験できます。
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 <条件> <文> end, の代わりに<条件> && <文>と書くことができます。 これは(<条件>ならば<文>)と読むことができます。 同様に if ! <条件> <文> endは <条件> || <文>と書くことができます。 これは(<条件>でなけば<文>)と読むことができます。
例えば、再帰的な階乗の計算はこのように定義できます。
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
trueif、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反復評価:ループ
式の反復評価をする構文は2つあります。 whileループとforループです。 whileループの例を示します。
julia> i = 1;
julia> while i <= 5
println(i)
global i += 1
end
1
2
3
4
5whileループは条件式を評価し(この場合はi <= 5)、それがtrueである限りwhileループの本体の評価を続けます。 条件式の評価が初めてfalseになった時、それ以降、本体の評価はまったく行いません。
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ループ形式では、iの見えるのは、forループの中だけで、外側や forループ以降からは見えません。 よって検査をするためには、新しい対話セッションを始めるか、別の変数名を使う必要があります。
julia> for j = 1:5
println(j)
end
1
2
3
4
5
julia> j
ERROR: UndefVarError: j not definedJuliaでの変数のスコープと挙動の詳細な説明と変数のスコープを参照してください。
一般に、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
5breakキーワードがなければ、上記の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ループは、統合して、各イテラブルオブジェクトの直積に対する、1つの外側のループにすることができます。
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キーワード使うように書き直したとすると、出力は変わるでしょう。 2番目と4番目の出力は0を含むでしょう。
例外処理
不測の事態が発生した場合、関数は呼び出し側に応えて、適切な値を返すことができないかもしれません。 そんな場合、例外的な状況に対する最善策は、プログラムを終了する一方で、状況を診断するエラーメッセージを表示することかもしれないし、 例外的な事態に対処するコードが用意されている場合は、そのコードを適切に動作させることかもしれません。
[](### Built-inException`s)
組込みの 例外
例外は不測の事態が発生した時に投げられます。 下記の組込みの例外のリストはすべて、通常の制御フローを中断します。
例えば、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[](### The [throw`](@ref) function)
throw関数
例外はthrowを使って明示的に生成することができます。 例えば、非負の数にのみ定義されている関数を、引数が負の時に DomainErrorをthrowすることで定義できます。
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:1DomainErrorに括弧を付けないと例外ではなく、例外の型になります。 Exceptionオブジェクトを補足する時に必要となり呼び出されます。
julia> typeof(DomainError(nothing)) <: Exception
true
julia> typeof(DomainError) <: Exception
falseさらに、例外の型の中には、エラー報告のために、1個以上の引数を取るものがあります。
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 scopefussy_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[](### Thetry/catch` statement)
try/catch 文
try/catch文では、例外を試行することができます。 例えば、例外を使って、実数と複素数のどちらかの平方根メソッドを要求次第で自動で呼び出す、独自の平方根関数を書くことができます。
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を0と比較する点です。 単に比較して分岐するよりも、はるかに例外は遅いのです。
try/catch文では、例外を変数に保存することができます。 以下の不自然な例では、xにインデックスがある場合には、xの第2要素の平方根を計算し、 そうでなければ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式を1行で書く場合は、注意が必要です。 以下のコードはエラーの場合にはxの値を返しません。
try bad() catch x end代替策として、セミコロンを使ったり、catchのあとに改行を入れたりします。
try bad() catch; x end
try bad()
catch
x
endtry/catch構文の威力は、深くネストした計算を直ちに巻き戻して、 関数呼び出しスタックのはるかに高水準まで戻って来ることが可能な点にあります。
[](###finally` Clauses)
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は後で中断したところから再開することができます。 一見、これは関数呼び出しと同じように見えるかもしれません。 しかし、2つの重要な違いがあります。 まず、タスクの切り替えにはメモリ領域を使用しません。 このため、切り替えるタスクの数をいくら増やしても、コールスタックを消費しません。 次に、タスクの切り替えは、どんな順番でもよく、関数呼び出しとは異なります。 関数呼び出しの場合は、呼び出される関数は、呼び出す関数に制御が戻る前に、実行を終了する必要があります。
この種の制御フローのを使うと、ある種の問題は簡単に解決できます。 ある種の問題では、関数呼び出しでは、様々な種類の作業を、自然に関連付けることができません。 なすべき仕事の「呼び出す側」と「呼び出される側」がはっきりしないものがあります。 例として挙げる「生産者/消費者」問題では、複雑な処理が値を生成する一方で、別の複雑な処理がそれを消費します。 消費者は値を得るために、単に生産者関数を呼び出せばいいわけではありません。 生産者には他にも生産すべき値があり、まだ値を返す準備ができていないかもしれないからです。 タスクを使うと、生産者と消費者は必要に応じて値をやり取りしながら、両者とも必要なだけ作動することができます。
Juliaにはこの問題を解決するためにChannelの仕組みがあります。 Channelは待機可能な先入先出のキューで複数のタスクを読取り・書込みが可能です。
生産者タスクを定義しましょう。 これは、put!の呼び出しによって値の生産を行います。 値を消費するには、生産者が新しいタスクを実行するようにスケジュールする必要があります。 1引数の関数を引数とするChannelの特殊なコンストラクタを使って、チャネルに束縛したタスクを実行することができます。 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"「生産者」は何回も値を返すことができる、というのはこの挙動の解釈の1つです。 put!の呼び出しの合間で、生産者の実行は中断し、制御が消費者に移ります。
戻り値のChannelはforループの中でイテラブルオブジェクトとして利用可能で、 この場合、ループの変数は、生産される値すべてを取ります。 チャネルが閉じるとループは終了します。
julia> for x in Channel(producer)
println(x)
end
start
2
4
6
8
stop生産者のチャネルを明示的に閉じる必要はないことに注意してください。 これは、ChannelがTaskを束縛しているために、 チャネルの開いている生涯期間が、束縛したタスクの生涯期間に関連付けられているからです。 チャネルオブジェクトはタスクが終了すると自動的に閉じられます。 タスクは複数のチャネルに束縛可能で、逆も成り立ちます。
Taskのコンストラクタの引数は、引数0個の関数ですが、 Channelのメソッドは、Channel型の引数1個を持つ関数で、 タスクを束縛したチャネルを生成します。 生産者をパラメータ化することは、よくあるパターンですが、この場合は引数が0個または1個の無名関数 を作るために部分関数の適用が必要です。
Taskオブジェクトに対して、これは、直接、または便利なマクロを使って行います。
function mytask(myarg)
...
end
taskHdl = Task(() -> mytask(7))
# or, equivalently
taskHdl = @task mytask(7)より高度な作業分配パターンを編成するために、bind やscheduleを TaskやChannelと一緒に使って、チャネルの集合と生産者/消費者のタスクの集合を、 明示的に連携させることができます。
現時点では、Juliaのタスクは、別々のCPUのコアにスケジュールされない点に注意してください。 真のカーネルスレッドの関しては並列コンピューティングのトピックで議論します。
コアタスク処理
タスクの切り替える方法について理解するために、低レベル関数のyieldtoを探索してみましょう。 yieldto(task,value)は現在のタスクを中断し、指定したtaskに切り替えます。 そして、タスクの最後のyieldto呼び出しに対して、指定したvalueを返します。 yieldtoだけがタスク型の制御フローに唯一必要な操作だということに注意してください。 関数を呼び出したり、値を返したりする代わりに、タスクを別のものに切り替えているだけです。 これはこの機能が「対象コルーチン」と呼ばれる理由です。 それぞれのタスクの切替が全く同じ仕組みを使っているからです。
yieldtoは強力ですが、たいていは直接呼び出されることはありません。これはなぜなのか、考えてみましょう。 現時点のタスクを中断する場合、おそらくいつか再開するでしょうが、それがいつで、再開に対して責任を持つタスクがどれなのかを知るには、 相当な調整が必要になるでしょう。 例えばput!やtake!は他を中断する操作ですが、チャネルと共に使う場合は、状態を保持して、誰がが消費者なのかを覚えます。 手動で消費者タスクを追跡する必要がないため、put!は低レベルのyieldtoよりも使いやすくなっています。
yieldtoの他に、タスクを効率的に使うために必要な基本的な別の関数がいくつかあります。
current_task現在実行しているタスクへの参照を取得します。istaskdoneタスクが終了しているかどうか問い合わせをします。istaskstartedタスクがまだ実行中かどうか問い合わせをします。task_local_storage現在のタスクに固有の、キーバリューの保存を操作します。
タスクとイベント
ほとんどのタスクの切替はI/O要求などのイベントを待機した結果として発生し、 JuliaのBaseライブラリに含まれるスケジューラによって実行されます。 スケジューラは実行可能なタスクのキューを保持し、イベントループを実行します。 このイベントループは、メッセージの到着など、外部のイベントに基づいてタスクを再開します。 イベントを待機する基本的な関数はwaitです。 オブジェクトの中にはwaitが実装されているものがいくつかあります。 例えば、Processオブジェクトの場合は、 waitは終了まで待機します。 wait暗黙裏に使われることもあります。 例えば、readを呼び出した際に、データが利用可能になるまで待機するために内部的に使うことがあります。
これらの場合すべてで、 waitは最終的にConditionオブジェクトに作用します。 このオブジェクトはタスクのキューの管理とタスクの再開に対する責務を負っています。 タスクがConditionに作用するwaitを呼び出すと、 タスクは非実行可能とマークされ、状態のキューに加えられ、スケジューラに切り替わります。 スケジューラは別のタスクを実行したり、外部イベントに対して待機するためにブロックしたりします。 すべてうまくいくと、最終的に、イベントハンドラは状態に作用する notify を呼び出し、 その結果、待機状態だったタスクが再び実行可能となります。
Taskを呼び出して明示的に生成したタスクは、始めはスケジューラに認識されていません。 このため、望むのであれば、yieldtoを使って手動でタスクを管理することも可能です。 しかし、イベントを待機するタスクは、予測されるように、イベントが発生すると自動的に再開します。 また、どんなイベントも待つことなく、可能な場合はいつでも、スケジューラがタスクを実行するようにもできます。 これは、 scheduleを呼び出したり、 @asyncマクロを使うことで可能です。 (詳細は並列コンピューティングを参照)
タスクの状態
タスクには実行状態を示すstateフィールドがあります。 Taskのstateは以下のシンボルのいずれかです。
| シンボル | 意味 |
|---|---|
:runnable | 現在実行中、または切替可能 |
:waiting | 特定のイベントを待機しているためブロックされている |
:queued | スケジューラの実行キューにあり、再開しようとしている |
:done | 実行が正常終了 |
:failed | 例外が捕捉されないまま終了 |