控制流

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 为真,相应的语句块将会被执行;否则就执行条件表达式 x > y ,如果结果为真, 相应的语句块将被执行;如果两个表达式都是假, 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

elseifelse 块是可选的。

注意,很短 (单行) 的条件语句在 Julia 中经常被写为短路求值的形式 (见下节).

如果条件表达式的值是除 truefalse 之外的值,会出错:

julia> if 1
         println("true")
       end
ERROR: type: non-boolean (Int64) used in boolean context

“问号表达式”语法 ?:if-elseif-else 语法相关,但是适用于单个表达式:

a ? b : c

? 之前的 a 是条件表达式,如果为 true ,就执行 : 之前的 b 表达式,如果为 false ,就执行 :c 表达式。

用问号表达式来重写,可以使前面的例子更加紧凑。先看一个二选一的例子:

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

三选一的例子需要链式调用问号表达式:

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 类似, : 前后的表达式,只有在对应条件表达式为 truefalse 时才执行:

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 中,只有 atrue 时才计算子表达式 b
  • 在表达式 a || b 中,只有 afalse 时才计算子表达式 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> (读作 <cond> 从而 <statement>)。 类似地,可以把 if ! <cond> <statement> end 写成 <cond> || <statement> (读作 <cond> 要不就 <statement>)。

例如, 递归阶乘可以这样写:

julia> function factorial(n::Int)
           n >= 0 || error("n must be non-negative")
           n == 0 && return 1
           n * factorial(n-1)
       end
factorial (generic function with 1 method)

julia> factorial(5)
120

julia> factorial(0)
1

julia> factorial(-1)
ERROR: n must be non-negative
 in factorial at none:2

短路求值运算符,可以使用 数学运算和基本函数 中介绍的位布尔运算符 &|

julia> f(1) & t(2)
1
2
false

julia> t(1) | t(2)
1
2
true

&&|| 的运算对象也必须是布尔值( truefalse )。除了最后一项外,在短路求值中使用非布尔值是一个错误:

julia> 1 && true
ERROR: type: non-boolean (Int64) used in boolean context

另一方面,短路求值的最后一项可以是任何类型的表达式。取决于之前的条件,它可以被求值并返回。

julia> true && (x = rand(2,2))
2x2 Array{Float64,2}:
 0.768448  0.673959
 0.940515  0.395453

julia> false && (x = rand(2,2))
false

重复求值: 循环

有两种循环表达式: while 循环和 for 循环。下面是 while 的例子:

julia> i = 1;

julia> while i <= 5
         println(i)
         i += 1
       end
1
2
3
4
5

上例也可以重写为 for 循环:

julia> for i = 1:5
         println(i)
       end
1
2
3
4
5

此处的 1:5 是一个 Range 对象,表示的是 1, 2, 3, 4, 5 序列。 for 循环遍历这些数,将其逐一赋给变量 iwhile 循环和 for 循环的另一区别是变量的作用域。如果在其它作用域中没有引入变量 i ,那么它仅存在于 for 循环中。不难验证:

julia> for j = 1:5
         println(j)
       end
1
2
3
4
5

julia> j
ERROR: j not defined

有关变量作用域,详见 变量的作用域

通常, for 循环可以遍历任意容器。这时,应使用另一个(但是完全等价的)关键词 in ,而不是 = ,它使得代码更易阅读:

julia> for i in [1,4,0]
         println(i)
       end
1
4
0

julia> for s in ["foo","bar","baz"]
         println(s)
       end
foo
bar
baz

手册中将介绍各种可迭代容器(详见 多维数组 )。

有时要提前终止 whilefor 循环。可以通过关键词 break 来实现:

julia> i = 1;

julia> while true
         println(i)
         if i >= 5
           break
         end
         i += 1
       end
1
2
3
4
5

julia> for i = 1:1000
         println(i)
         if i >= 5
           break
         end
       end
1
2
3
4
5

有时需要中断本次循环,进行下一次循环,这时可以用关键字 continue

julia> for i = 1:10
         if i % 3 != 0
           continue
         end
         println(i)
       end
3
6
9

多层 for 循环可以被重写为一个外层循环,迭代类似于笛卡尔乘积的形式:

julia> for i = 1:2, j = 3:4
         println((i, j))
       end
(1,3)
(1,4)
(2,3)
(2,4)

这种情况下用 break 会直接跳出所有循环,而不仅仅是最内层的循环。

异常处理

当遇到意外条件时,函数可能无法给调用者返回一个合理值。这时,要么终止程序,打印诊断错误信息;要么程序员编写异常处理。

内置异常 Exception

如果程序遇到意外条件,异常将会被抛出。表中列出内置异常。

Exception
ArgumentError
BoundsError
DivideError
DomainError
EOFError
ErrorException
InexactError
InterruptException
KeyError
LoadError
MemoryError
MethodError
OverflowError
ParseError
SystemError
TypeError
UndefRefError
UndefVarError

例如,当对负实数使用内置的 sqrt 函数时,将抛出 DomainError()

julia> sqrt(-1)
ERROR: DomainError
sqrt will only return a complex result if called with a complex argument.
try sqrt(complex(x))
 in sqrt at math.jl:131

你可以用以下方式定义自己的异常:

julia> type MyCustomException <: Exception end

throw 函数

可以使用 throw 函数显式创建异常。例如,某个函数只对非负数做了定义,如果参数为负数,可以抛出 DomaineError 异常:

julia> f(x) = x>=0 ? exp(-x) : throw(DomainError())
f (generic function with 1 method)

julia> f(1)
0.36787944117144233

julia> f(-1)
ERROR: DomainError
 in f at none:1

注意, DomainError 使用时需要使用带括号的形式,否则返回的并不是异常,而是异常的类型。必须带括号才能返回 Exception 对象:

julia> typeof(DomainError()) <: Exception
true

julia> typeof(DomainError) <: Exception
false

另外,某些异常接受一个或多个参数:

julia> throw(UndefVarError(:x))
ERROR: x not defined

仿照 UndefVarError 定义的方式,这个机制也可以在自定义异常类型中使用:

julia> type MyUndefVarError <: Exception
           var::Symbol
       end
julia> Base.showerror(io::IO, e::MyUndefVarError) = print(io, e.var, " not defined");

error 函数

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
 in fussy_sqrt at none:1

当对负数调用 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
 in verbose_fussy_sqrt at none:3

warninfo 函数

Julia 还提供一些函数,用来向标准错误 I/O 输出一些消息,但不抛出异常,因而并不会打断程序的执行:

julia> info("Hi"); 1+1
INFO: Hi
2

julia> warn("Hi"); 1+1
WARNING: Hi
2

julia> error("Hi"); 1+1
ERROR: Hi
 in error at error.jl:21

try/catch 语句

try/catch 语句可以用于处理一部分预料中的异常 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

但是处理异常比正常采用分支来处理,会慢得多。

try/catch 语句使用时也可以把异常赋值给某个变量。例如:

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
 in sqrt_second at none:7

注意,紧跟 catch 的符号会作为异常的名字,所以在将 try/catch 写在单行内的时候需要特别注意。下面的代码 不会 在发生错误的时候返回 x 的值:

try bad() catch x end

相对的,使用分号或在 catch 后另起一行:

try bad() catch; x end

try bad()
catch
  x
end

Julia 还提供了更高级的异常处理函数 rethrowbacktracecatch_backtrace

finally 语句

在改变状态或者使用文件等资源时,通常需要在操作执行完成时做清理工作(比如关闭文件)。异常的存在使得这样的任务变得复杂,因为异常会导致程序提前退出。关键字 finally 可以解决这样的问题,无论程序是怎样退出的, finally 语句总是会被执行。

例如, 下面的程序说明了怎样保证打开的文件总是会被关闭:

f = open("file")
try
    # operate on file f
finally
    close(f)
end

当程序执行完 try 语句块(例如因为执行到 return 语句,或者只是正常完成), close 语句将会被执行。如果 try 语句块因为异常提前退出,异常将会继续传播。 catch 语句可以和 tryfinally 一起使用。这时。 finally 语句将会在 catch 处理完异常之后执行。

任务(也称为协程)

任务是一种允许计算灵活地挂起和恢复的控制流,有时也被称为对称协程、轻量级线程、协同多任务等。

如果一个计算(比如运行一个函数)被设计为 Task ,有可能因为切换到其它 Task 而被中断。原先的 Task 在以后恢复时,会从原先中断的地方继续工作。切换任务不需要任何空间,同时可以有任意数量的任务切换,而不需要考虑堆栈问题。任务切换与函数调用不同,可以按照任何顺序来进行。

任务比较适合生产者-消费者模式,一个过程用来生产值,另一个用来消费值。消费者不能简单的调用生产者来得到值,因为两者的执行时间不一定协同。在任务中,两者则可以 正常运行。

Julia 提供了 produceconsume 函数来解决这个问题。生产者调用 produce 函数来生产值:

julia> function producer()
         produce("start")
         for n=1:4
           produce(2n)
         end
         produce("stop")
       end;

要消费生产的值,先对生产者调用 Task 函数,然后对返回的对象重复调用 consume

julia> p = Task(producer);

julia> consume(p)
"start"

julia> consume(p)
2

julia> consume(p)
4

julia> consume(p)
6

julia> consume(p)
8

julia> consume(p)
"stop"

可以在 for 循环中迭代任务,生产的值被赋值给循环变量:

julia> for x in Task(producer)
         println(x)
       end
start
2
4
6
8
stop

注意 Task() 函数的参数,应为零参函数。生产者常常是参数化的,因此需要为其构造零参 匿名函数 。可以直接写,也可以调用宏:

function mytask(myarg)
    ...
end

taskHdl = Task(() -> mytask(7))
# 也可以写成
taskHdl = @task mytask(7)

produceconsume 但它并不在不同的 CPU 发起线程。我们将在 并行计算 中,讨论真正的内核线程。

核心任务操作

尽管 produceconsume 已经阐释了任务的本质,他们实际上是由库函数调用更原始的函数 yieldto 实现的。 yieldto(task,value) 挂起当前任务,切换到指定的 task , 并使这个 task 的上一次 yeidlto 返回指定的 value 。注意 yieldto 是任务风格的控制流唯一需要的操作;取代调用和返回,我们只用在不同的任务之间切换即可。这就是为什么 这个特性被称做 “对称式协程”:每一个任务的切换都是用相同的机制。

尽管 yeildto 很强大,但是大多数任务并不直接调用它。这当中的原因可以理解。当你从当前的任务 切换走,你一般还会切换回来。然而正确的处理切换的时机和任务需要相当的协调。例如, procude 需要保持某个状态来记录消费者。无需手动地记录正在消费的任务让 produceyieldto 更容易使用。

yieldto 之外,我们也需要一些其他的基本函数以高效地使用任务。 current_task() 获得当前运行任务的引用。 istaskdone(t) 查询任务是否终止。 istaskstarted(t) 查询任务是否启动。 task_local_storage 处理当前任务的键值储存。

任务与事件

大多数任务的切换发生在等待 I/O 请求这样的事件的时候,并由标准库的调度器完成。调度器记录正在运行 的任务的队列,并执行一个循环来根据外部事件(比如消息到达)重启任务。

处理等待事件的基本函数是 wait 。有几种对象实现了 wait,比如对于 Process 对象, wait 会等待它终止。更多的时候 wait 是隐式的, 比如 wait 可以发生在调用 read 的时候,以等待可用数据。

在所有的情况中, wait 最终会操作在一个负责将任务排队和重启的 Condition 对象上。 当任务在 Condition 上调用 wait ,任务会被标记为不可运行,并被加入到 Condition 的队列中,再切换至调度器。调度器会选取另一个任务来运行,或者等待外部事件。如果一切正常,最终一个 事件句柄会在 Condition 上调用 notify ,使正在等待的任务再次变得可以运行。

调用 Task 可以生成一个未被调度器管理的任务,这允许你用 yieldto 手动管理任务。不管怎样, 当这样的任务正在等待事件时,事件一旦发生,它仍然会自动重启。而且任何时候你都可以调用 schedule(task) 或者用宏 @schedule@async 来让调度器来运行一个任务, 而不用去等待任何事件。(参见 并行计算)

任务状态

任务包含一个 state 域,它用来描述任务的执行状态。任务状态取如下的几种符号中的一种:

符号 意义
:runnable 任务正在运行,或可被切换到该任务
:waiting 正在阻塞等待事件
:queued 在调度器的队列中将要重新开始运行
:done 成功执行完毕
:failed 由于未处理的异常而终止