制御フロー
Julia は、さまざまな制御フロー構造を提供します:
- Compound Expressions:
begin
and(;)
. - Conditional Evaluation:
if
-elseif
-else
and?:
(ternary operator). - 短絡評価:
&&
,||
と比較の連鎖。 - Repeated Evaluation: Loops:
while
andfor
. - 例外処理:
try
-catch
、error
とthrow
。 - Tasks (aka Coroutines):
yieldto
.
The first five control flow mechanisms are standard to high-level programming languages. Task
s are not so standard: they provide non-local control flow, making it possible to switch between temporarily-suspended computations. This is a powerful construct: both exception handling and cooperative multitasking are implemented in Julia using tasks. Everyday programming requires no direct usage of tasks, but certain problems can be solved much more easily by using tasks.
Compound Expressions
Sometimes it is convenient to have a single expression which evaluates several subexpressions in order, returning the value of the last subexpression as its value. There are two Julia constructs that accomplish this: begin
blocks and (;)
chains. The value of both compound expression constructs is that of the last subexpression. Here's an example of a begin
block:
julia> z = begin
x = 1
y = 2
x + y
end
3
Since these are fairly small, simple expressions, they could easily be placed onto a single line, which is where the (;)
chain syntax comes in handy:
julia> z = (x = 1; y = 2; x + y)
3
This syntax is particularly useful with the terse single-line function definition form introduced in Functions. Although it is typical, there is no requirement that begin
blocks be multiline or that (;)
chains be single-line:
julia> begin x = 1; y = 2; x + y end
3
julia> (x = 1;
y = 2;
x + y)
3
Conditional Evaluation
Conditional evaluation allows portions of code to be evaluated or not evaluated depending on the value of a boolean expression. Here is the anatomy of the if
-elseif
-else
conditional syntax:
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
If the condition expression x < y
is true
, then the corresponding block is evaluated; otherwise the condition expression x > y
is evaluated, and if it is true
, the corresponding block is evaluated; if neither expression is true, the else
block is evaluated. Here it is in action:
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
The elseif
and else
blocks are optional, and as many elseif
blocks as desired can be used. The condition expressions in the if
-elseif
-else
construct are evaluated until the first one evaluates to true
, after which the associated block is evaluated, and no further condition expressions or blocks are evaluated.
if
blocks are "leaky", i.e. they do not introduce a local scope. This means that new variables defined inside the if
clauses can be used after the if
block, even if they weren't defined before. So, we could have defined the test
function above as
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.
The variable relation
is declared inside the if
block, but used outside. However, when depending on this behavior, make sure all possible code paths define a value for the variable. The following change to the above function results in a runtime error
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
blocks also return a value, which may seem unintuitive to users coming from many other languages. This value is simply the return value of the last executed statement in the branch that was chosen, so
julia> x = 3
3
julia> if x > 0
"positive!"
else
"negative..."
end
"positive!"
Note that very short conditional statements (one-liners) are frequently expressed using Short-Circuit Evaluation in Julia, as outlined in the next section.
Unlike C, MATLAB, Perl, Python, and Ruby – but like Java, and a few other stricter, typed languages – it is an error if the value of a conditional expression is anything but true
or false
:
julia> if 1
println("true")
end
ERROR: TypeError: non-boolean (Int64) used in boolean context
This error indicates that the conditional was of the wrong type: Int64
rather than the required Bool
.
The so-called "ternary operator", ?:
, is closely related to the if
-elseif
-else
syntax, but is used where a conditional choice between single expression values is required, as opposed to conditional execution of longer blocks of code. It gets its name from being the only operator in most languages taking three operands:
a ? b : c
The expression a
, before the ?
, is a condition expression, and the ternary operation evaluates the expression b
, before the :
, if the condition a
is true
or the expression c
, after the :
, if it is false
. Note that the spaces around ?
and :
are mandatory: an expression like a?b:c
is not a valid ternary expression (but a newline is acceptable after both the ?
and the :
).
The easiest way to understand this behavior is to see an example. In the previous example, the println
call is shared by all three branches: the only real choice is which literal string to print. This could be written more concisely using the ternary operator. For the sake of clarity, let's try a two-way version first:
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
If the expression x < y
is true, the entire ternary operator expression evaluates to the string "less than"
and otherwise it evaluates to the string "not less than"
. The original three-way example requires chaining multiple uses of the ternary operator together:
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
To facilitate chaining, the operator associates from right to left.
It is significant that like if
-elseif
-else
, the expressions before and after the :
are only evaluated if the condition expression evaluates to true
or false
, respectively:
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"
短絡評価
Short-circuit evaluation is quite similar to conditional evaluation. The behavior is found in most imperative programming languages having the &&
and ||
boolean operators: in a series of boolean expressions connected by these operators, only the minimum number of expressions are evaluated as are necessary to determine the final boolean value of the entire chain. Explicitly, this means that:
- In the expression
a && b
, the subexpressionb
is only evaluated ifa
evaluates totrue
. - In the expression
a || b
, the subexpressionb
is only evaluated ifa
evaluates tofalse
.
The reasoning is that a && b
must be false
if a
is false
, regardless of the value of b
, and likewise, the value of a || b
must be true if a
is true
, regardless of the value of b
. Both &&
and ||
associate to the right, but &&
has higher precedence than ||
does. It's easy to experiment with this behavior:
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
You can easily experiment in the same way with the associativity and precedence of various combinations of &&
and ||
operators.
This behavior is frequently used in Julia to form an alternative to very short if
statements. Instead of if <cond> <statement> end
, one can write <cond> && <statement>
(which could be read as: <cond> and then <statement>). Similarly, instead of if ! <cond> <statement> end
, one can write <cond> || <statement>
(which could be read as: <cond> or else <statement>).
For example, a recursive factorial routine could be defined like this:
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
Just like condition expressions used in if
, elseif
or the ternary operator, the operands of &&
or ||
must be boolean values (true
or false
). Using a non-boolean value anywhere except for the last entry in a conditional chain is an error:
julia> 1 && true
ERROR: TypeError: non-boolean (Int64) used in boolean context
On the other hand, any type of expression can be used at the end of a conditional chain. It will be evaluated and returned depending on the preceding conditionals:
julia> true && (x = (1, 2, 3))
(1, 2, 3)
julia> false && (x = (1, 2, 3))
false
Repeated Evaluation: Loops
There are two constructs for repeated evaluation of expressions: the while
loop and the for
loop. Here is an example of a while
loop:
julia> i = 1;
julia> while i <= 5
println(i)
global i += 1
end
1
2
3
4
5
The while
loop evaluates the condition expression (i <= 5
in this case), and as long it remains true
, keeps also evaluating the body of the while
loop. If the condition expression is false
when the while
loop is first reached, the body is never evaluated.
The for
loop makes common repeated evaluation idioms easier to write. Since counting up and down like the above while
loop does is so common, it can be expressed more concisely with a for
loop:
julia> for i = 1:5
println(i)
end
1
2
3
4
5
Here the 1:5
is a range object, representing the sequence of numbers 1, 2, 3, 4, 5. The for
loop iterates through these values, assigning each one in turn to the variable i
. One rather important distinction between the previous while
loop form and the for
loop form is the scope during which the variable is visible. If the variable i
has not been introduced in another scope, in the for
loop form, it is visible only inside of the for
loop, and not outside/afterwards. You'll either need a new interactive session instance or a different variable name to test this:
julia> for j = 1:5
println(j)
end
1
2
3
4
5
julia> j
ERROR: UndefVarError: j not defined
See Scope of Variables for a detailed explanation of variable scope and how it works in Julia.
In general, the for
loop construct can iterate over any container. In these cases, the alternative (but fully equivalent) keyword in
or ∈
is typically used instead of =
, since it makes the code read more clearly:
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
Various types of iterable containers will be introduced and discussed in later sections of the manual (see, e.g., Multi-dimensional Arrays).
It is sometimes convenient to terminate the repetition of a while
before the test condition is falsified or stop iterating in a for
loop before the end of the iterable object is reached. This can be accomplished with the break
keyword:
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
Without the break
keyword, the above while
loop would never terminate on its own, and the for
loop would iterate up to 1000. These loops are both exited early by using break
.
In other circumstances, it is handy to be able to stop an iteration and move on to the next one immediately. The continue
keyword accomplishes this:
julia> for i = 1:10
if i % 3 != 0
continue
end
println(i)
end
3
6
9
This is a somewhat contrived example since we could produce the same behavior more clearly by negating the condition and placing the println
call inside the if
block. In realistic usage there is more code to be evaluated after the continue
, and often there are multiple points from which one calls continue
.
Multiple nested for
loops can be combined into a single outer loop, forming the cartesian product of its iterables:
julia> for i = 1:2, j = 3:4
println((i, j))
end
(1, 3)
(1, 4)
(2, 3)
(2, 4)
With this syntax, iterables may still refer to outer loop variables; e.g. for i = 1:n, j = 1:i
is valid. However a break
statement inside such a loop exits the entire nest of loops, not just the inner one. Both variables (i
and j
) are set to their current iteration values each time the inner loop runs. Therefore, assignments to i
will not be visible to subsequent iterations:
julia> for i = 1:2, j = 3:4
println((i, j))
i = 0
end
(1, 3)
(1, 4)
(2, 3)
(2, 4)
If this example were rewritten to use a for
keyword for each variable, then the output would be different: the second and fourth values would contain 0
.
例外処理
予想外の状況が発生すると、関数が呼び出し元に妥当な値を返すことができないことがあります。このような事態に対する最善の策は、プログラムを終了させることかもしれませんし、状況報告のエラーメッセージを出力することかもしれません。あるいは、もしプログラマがこのような例外的な状況を上手く扱うコードを提供しているならば、そのコードが適切なアクションを実行できるようにするのがよいかもしれません。
組み込みの例外
例外は、予期しない状態が発生したときにスローされます。以下に示す組み込みのException
は、通常の制御フローを中断します。
たとえば、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
を使用して明示的に発生させられます。たとえば、負以外の数値に対してのみ定義された関数は、引数が負の場合は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
は括弧をつけない場合は、例外ではなく、例外の型を表す点に注意してください。 例外オブジェクトを取得するには、括弧を付けて関数呼び出しを行う必要があります:
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
が別の関数から負の値で呼び出された場合、(fussy_sqrt
以降の)関数呼び出し続けようとせず、すぐにreturnして、対話型セッションにエラー メッセージを表示します:
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
文
The try/catch
statement allows for Exception
s to be tested for, and for the graceful handling of things that may ordinarily break your application. For example, in the below code the function for square root would normally throw an exception. By placing a try/catch
block around it we can mitigate that here. You may choose how you wish to handle this exception, whether logging it, return a placeholder value or as in the case below where we just printed out a statement. One thing to think about when deciding how to handle unexpected situations is that using a try/catch
block is much slower than using conditional branching to handle those situations. Below there are more examples of handling exceptions with a try/catch
block:
julia> try
sqrt("ten")
catch e
println("You should have entered a numeric value")
end
You should have entered a numeric value
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
の威力は、深くネストされた計算から、関数コールのスタックにおいてはるかに上のレベルに飛び越えて戻ることができる点にあります。エラーが発生していない場合でも、スタックを飛び越えて戻り、より上の階層に値を渡せる機能は欲しい物です。Juliaは、rethrow
、[backtrace
(@ref)]、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
がエラーを処理した後に実行されます。
Tasks (aka Coroutines)
Tasks are a control flow feature that allows computations to be suspended and resumed in a flexible manner. This feature is sometimes called by other names, such as symmetric coroutines, lightweight threads, cooperative multitasking, or one-shot continuations.
When a piece of computing work (in practice, executing a particular function) is designated as a Task
, it becomes possible to interrupt it by switching to another Task
. The original Task
can later be resumed, at which point it will pick up right where it left off. At first, this may seem similar to a function call. However there are two key differences. First, switching tasks does not use any space, so any number of task switches can occur without consuming the call stack. Second, switching among tasks can occur in any order, unlike function calls, where the called function must finish executing before control returns to the calling function.
This kind of control flow can make it much easier to solve certain problems. In some problems, the various pieces of required work are not naturally related by function calls; there is no obvious "caller" or "callee" among the jobs that need to be done. An example is the producer-consumer problem, where one complex procedure is generating values and another complex procedure is consuming them. The consumer cannot simply call a producer function to get a value, because the producer may have more values to generate and so might not yet be ready to return. With tasks, the producer and consumer can both run as long as they need to, passing values back and forth as necessary.
Julia provides a Channel
mechanism for solving this problem. A Channel
is a waitable first-in first-out queue which can have multiple tasks reading from and writing to it.
Let's define a producer task, which produces values via the put!
call. To consume values, we need to schedule the producer to run in a new task. A special Channel
constructor which accepts a 1-arg function as an argument can be used to run a task bound to a channel. We can then take!
values repeatedly from the channel object:
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"
One way to think of this behavior is that producer
was able to return multiple times. Between calls to put!
, the producer's execution is suspended and the consumer has control.
The returned Channel
can be used as an iterable object in a for
loop, in which case the loop variable takes on all the produced values. The loop is terminated when the channel is closed.
julia> for x in Channel(producer)
println(x)
end
start
2
4
6
8
stop
Note that we did not have to explicitly close the channel in the producer. This is because the act of binding a Channel
to a Task
associates the open lifetime of a channel with that of the bound task. The channel object is closed automatically when the task terminates. Multiple channels can be bound to a task, and vice-versa.
While the Task
constructor expects a 0-argument function, the Channel
method which creates a channel bound task expects a function that accepts a single argument of type Channel
. A common pattern is for the producer to be parameterized, in which case a partial function application is needed to create a 0 or 1 argument anonymous function.
For Task
objects this can be done either directly or by use of a convenience macro:
function mytask(myarg)
...
end
taskHdl = Task(() -> mytask(7))
# or, equivalently
taskHdl = @task mytask(7)
To orchestrate more advanced work distribution patterns, bind
and schedule
can be used in conjunction with Task
and Channel
constructors to explicitly link a set of channels with a set of producer/consumer tasks.
Note that currently Julia tasks are not scheduled to run on separate CPU cores. True kernel threads are discussed under the topic of Parallel Computing.
Core task operations
Let us explore the low level construct yieldto
to understand how task switching works. yieldto(task,value)
suspends the current task, switches to the specified task
, and causes that task's last yieldto
call to return the specified value
. Notice that yieldto
is the only operation required to use task-style control flow; instead of calling and returning we are always just switching to a different task. This is why this feature is also called "symmetric coroutines"; each task is switched to and from using the same mechanism.
yieldto
is powerful, but most uses of tasks do not invoke it directly. Consider why this might be. If you switch away from the current task, you will probably want to switch back to it at some point, but knowing when to switch back, and knowing which task has the responsibility of switching back, can require considerable coordination. For example, put!
and take!
are blocking operations, which, when used in the context of channels maintain state to remember who the consumers are. Not needing to manually keep track of the consuming task is what makes put!
easier to use than the low-level yieldto
.
In addition to yieldto
, a few other basic functions are needed to use tasks effectively.
current_task
gets a reference to the currently-running task.istaskdone
queries whether a task has exited.istaskstarted
queries whether a task has run yet.task_local_storage
manipulates a key-value store specific to the current task.
Tasks and events
Most task switches occur as a result of waiting for events such as I/O requests, and are performed by a scheduler included in Julia Base. The scheduler maintains a queue of runnable tasks, and executes an event loop that restarts tasks based on external events such as message arrival.
The basic function for waiting for an event is wait
. Several objects implement wait
; for example, given a Process
object, wait
will wait for it to exit. wait
is often implicit; for example, a wait
can happen inside a call to read
to wait for data to be available.
In all of these cases, wait
ultimately operates on a Condition
object, which is in charge of queueing and restarting tasks. When a task calls wait
on a Condition
, the task is marked as non-runnable, added to the condition's queue, and switches to the scheduler. The scheduler will then pick another task to run, or block waiting for external events. If all goes well, eventually an event handler will call notify
on the condition, which causes tasks waiting for that condition to become runnable again.
A task created explicitly by calling Task
is initially not known to the scheduler. This allows you to manage tasks manually using yieldto
if you wish. However, when such a task waits for an event, it still gets restarted automatically when the event happens, as you would expect. It is also possible to make the scheduler run a task whenever it can, without necessarily waiting for any events. This is done by calling schedule
, or using the @async
macro (see Parallel Computing for more details).
Task states
Tasks have a state
field that describes their execution status. A Task
state
is one of the following symbols:
Symbol | Meaning |
---|---|
:runnable | Currently running, or able to run |
:done | Successfully finished executing |
:failed | Finished with an uncaught exception |