流程控制
Julia 提供了大量的流程控制构件:
- [复合表达式]
有时一个表达式能够有序地计算若干子表达式,并返回最后一个子表达式的值作为它的值是很方便的。Julia 有两个组件来完成这个:
begin
代码块 和(;)
链。这两个复合表达式组件的值都是最后一个子表达式的值。下面是一个begin
代码块的例子:julia> z = begin x = 1 y = 2 x + y end 3
因为这些是非常简短的表达式,它们可以简单地被放到一行里,这也是
(;)
链的由来:julia> z = (x = 1; y = 2; x + y) 3
这个语法在定义简洁的单行函数的时候特别有用,参见 [函数]
条件表达式(Conditional evaluation)可以根据布尔表达式的值,让部分代码被执行或者不被执行。下面是对
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
,则执行对应的代码块;如果没有表达式是 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!"
需要注意的是,在 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
, 是一个条件表达式,如果条件a
是true
,三元运算符计算在:
之前的表达式b
;如果条件a
是false
,则执行:
后面的表达式c
。注意,?
和:
旁边的空格是强制的,像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
是false
,那么无论b
的值是多少,a && b
一定是false
。同理,如果a
是true
,那么无论b
的值是多少,a || 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
语句的替代。 可以用<cond> && <statement>
(可读为: and then )来替换if <cond> <statement> end
。 类似的, 可以用<cond> || <statement>
(可读为: or else )来替换if ! <cond> <statement> 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
[重复执行:循环]
有两个用于重复执行表达式的组件:
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
循环的主体部分。当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
进行赋值。for
循环与之前while
循环的一个非常重要区别是作用域,即变量的可见性。如果变量i
没有在另一个作用域里引入,在for
循环内,它就只在for
循环内部可见,在外部和后面均不可见。你需要一个新的交互式会话实例或者一个新的变量名来测试这个特性:julia> for j = 1:5 println(j) end 1 2 3 4 5 julia> j ERROR: UndefVarError: j not defined
参见[变量作用域]
Task
是一种允许计算以更灵活的方式被中断或者恢复的流程控制特性。这个特性有时被叫做其它名字,例如,对称协程(symmetric coroutines),轻量级线程(lightweight threads),合作多任务处理(cooperative multitasking),或者单次续延(one-shot continuations)。当一部分计算任务(在实际中,执行一个特定的函数)可以被设计成一个
Task
时,就可以中断它,并切换到另一个Task
。原本的Task
可以恢复到它上次中断的地方,并继续执行。第一眼感觉,这个跟函数调用很类似。但是有两个关键的区别。首先,是切换Task
并不使用任何空间,所以任意数量的Task
切换都不会使用调用栈(call stack)。其次,Task
可以以任意次序切换,而不像函数调用那样,被调用函数必须在返回主调用函数之前结束执行。这种流程控制的方式使得解决一个特定问题更简便。在一些问题中,多个需求并不是有函数调用来自然连接的;在需要完成的工作之间并没有明确的“调用者”或者“被调用者”。一个例子是生产-消费问题,一个复杂的流程产生数据,另一个复杂的流程消费他们。消费者不能简单的调用生产函数来获得一个值,因为生产者可能有更多的值需要创建,还没有准备好返回。用
Task
的话,生产者和消费者能同时运行他们所需要的任意时间,根据需要传递值回来或者过去。Julia 提供了
Channel
机制来解决这个问题。一个Channel
是一个先进先出的队列,允许多个Task
对它可以进行读和写。让我们定义一个生产者任务,调用
put!
来生产数值。为了消费数值,我们需要对生产者开始新任务进行排班。可以使用一个特殊的Channel
组件来运行一个与其绑定的Task
,它能接受单参数函数作为其参数,然后可以用take!
从Channel
对象里不断地提取值: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"
一种思考这种行为的方式是,“生产者”能够多次返回。在两次调用
put!
之间,生产者的执行是挂起的,此时由消费者接管控制。返回的
Channel
可以被用作一个for
循环的迭代对象,此时循环变量会依次取到所有产生的值。当Channel
关闭时,循环就会终止。julia> for x in Channel(producer) println(x) end start 2 4 6 8 stop
注意我们并不需要显式地在生产者中关闭
Channel
。这是因为Channel
对Task
的绑定同时也意味着Channel
的生命周期与绑定的Task
一致。当Task
结束时,Channel
对象会自动关闭。多个Channel
可以绑定到一个Task
,反之亦然。尽管
Task
的构造函数只能接受一个“无参函数”,但Channel
方法会创建一个与Channel
绑定的Task
,并令其可以接受Channel
类型的单参数函数。一个通用模式是对生产者参数化,此时需要一个部分函数应用来创建一个无参,或者单参的[匿名函数](http://127.0.0.5/@ref man-anonymous-functions)。对于
Task
对象,可以直接用,也可以为了方便用宏。function mytask(myarg) ... end taskHdl = Task(() -> mytask(7)) # or, equivalently taskHdl = @task mytask(7)
为了安排更高级的工作分配模式,
bind
和schedule
可以与Task
和Channel
构造函数配合使用,显式地连接一些Channel
和生产者或消费者Task
。注意目前 Julia 的
Task
并不分配到或者运行在不同的 CPU 核心上。真正的内核进程将在并行计算进行讨论。Task
相关的核心操作让我们来学习底层构造函数
yieldto
来理解Task
是如何切换工作的。yieldto(task,value)
会中断当前的Task
,并切换到特定的Task
,并且Task
的最后一次yieldto
调用会有特定的返回值
。注意yieldto
是唯一一个需要用任务类型的流程控制的操作,仅需要切换到不同的Task
,而不需要调用或者返回。这也就是为什么这个特性会被叫做“对称协程(symmetric coroutines)”;每一个Task
以相同的机制进行切换或者被切换。yieldto
功能强大,但大多数Task
的使用都不会直接调用它。思考为什么会这样。如果你切换当前Task
,你很可能会在某个时候想切换回来。但知道什么时候切换回来和那个Task
负责切换回来需要大量的协调。例如,put!
和take!
是阻塞操作,当在渠道环境中使用时,维持状态以记住消费者是谁。不需要人为地记录消费Task
,正是使得put!
比底层yieldto
易用的原因。除了
yieldto
之外,也需要一些其它的基本函数来更高效地使用Task
。current_task
获取当前运行Task
的索引。istaskdone
查询一个Task
是否退出.istaskstarted
查询一个Task
是否已经开始运行。task_local_storage
操纵针对当前Task
的键值存储。
Task
和事件多数
Task
切换是在等待如 I/O 请求的事件,由 Julia Base 里的调度器执行。调度器维持一个可运行Task
的队列,并执行一个事件循环,来根据例如收到消息等外部事件来重启Task
。等待一个事件的基本函数是
wait
。很多对象都实现了wait
函数;例如,给定一个Process
对象,wait
将等待它退出。wait
通常是隐式的,例如,wait
可能发生在调用read
时等待数据可用。在所有这些情况下,
wait
最终会操作一个Condition
对象,由它负责排队和重启Task
。当Task
在一个Condition
上调用wait
时,该 Task 就被标记为不可执行,加到条件的队列中,并切回调度器。调度器将选择另一个Task
来运行,或者阻止外部事件的等待。如果所有运行良好,最终一个事件处理器将在这个条件下调用notify
,使得等待该条件的Task
又变成可运行。调用
Task
显式创建的Task
对于调度器时来说一开始时不知道的。如果你希望的话,你可以使用yieldto
来人为管理Task
。但是当这种Task
等待一个事件时,正如期待的那样,当事件发生时,它将自动重启。也能由调度器在任何可能的时候运行一个Task
,而无需等待任何事件。这可以调用schedule
,或者使用@async
宏(见并行计算中的详细说明)。Task
的状态Task
由state
属性来描述他们的执行状态。Task
state
有:符号 含义 :runnable
正在运行,或者可以被切换到 :waiting
被阻塞,等待一个特定事件 :queued
处在调度器中的运行队列中,即将被重启 :done
成功结束执行 :failed
以一个没被捕获的异常结束 - 在表达式
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论