制御フロー
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 y
elseif
と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
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
反復評価:ループ
式の反復評価をする構文は2つあります。 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
になった時、それ以降、本体の評価はまったく行いません。
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 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
ループは、統合して、各イテラブルオブジェクトの直積に対する、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-in
Exception`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:1
DomainError
に括弧を付けないと例外ではなく、例外の型になります。 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 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
[](### The
try/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
end
try/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 | 例外が捕捉されないまま終了 |