Julia提供各种各样的控制流概念:
- 复合表达式:
begin
和(;)
。 - 条件计算:
if-elseif-else
和?:
(三元操作符)。 - 短路计算:
&&
、||
和链式比较。 - 重复计算(循环):
while
和for
。 - 异常处理:
try-catch-finally
、error
和throw
。 - 任务(即协程):
yieldto
。
前五个控制流机制对高级编程语言来说是标配的。 协程并非标配:提供非本地控制流,让临时切换计算挂起、恢复成为可能。 这是强大的概念:Julia中异常处理和多任务合作都是基于协程实现的。 日藏编程不需要直接适用任务,但特定问题可通过任务更简单地得到解决。
有些时候,单个表达式中按顺序计算若干子表达式、返回最后一个表达式的值作为结果是便捷的。
Julia中有两种设计来完成:begin
块和(;)
链。
这两种复合表达式构建的值都是最后一个子表达式的。这儿展示一个begin
块的例子:
julia> z = begin
x = 9527
y = 1314
x + y
end
10841
由于这些表达式相当小且简单,很容易放置在单行,正式(;)
链语法随手拈来:
julia> z = (x = 9527; y = 1314; x + y)
10841
【函数】简明单行函数定义形式兼备该语法特别有用。
尽管这是典型的,但是并未要求begin
块是多行或(;)
链是单行的。
julia> begin x = "huaan"; y = "qiuxiang"; x * " " * y end
"huaan qiuxiang"
julia> (x = "huaan";
y = "qiuxiang";
x * " " * y)
"huaan qiuxiang"
条件计算允许部分代码被计算或不被计算,取决于一个布尔型表达式的值。
下面演示if-elseif-else
条件语法的脉络(骨骼):
julia> function toy(x, y)
if x < y
println("x is less than y")
elseif x > y
println("x is more than y")
else
println("x is equal to y")
end
end
toy (generic function with 1 method)
julia> toy(9527, 1314)
x is more than y
julia> toy(1314, 9527)
x is less than y
julia> toy(0, 0)
x is equal to y
如果条件表达式x < y
是true
(成立),则相应的代码块被计算;否则条件表达式x > y
被计算且如果是true
(成立),则相应的代码块被计算;若前两个条件都不成立,则else
代码块被执行。
其中elseif
和else
块是可选的,并且许多elseif
块渴望被用到。
在if-elseif-else
概念中条件表达式挨个儿被计算,直到有一个成立(true
),对应的代码块被执行后,其余条件表达式及其对应的代码块都不再被计算。
注意if
代码块有“漏洞”,就是说条件表达式并不引进本地范围,这意味着在if
子句中定义新的变量,之后也可用,即使先前并未定义该变量。因此可以这么定义toy
函数:
julia> function toy(x, y)
if x < y
relation = "less than"
elseif x == y
relation = "equal to"
else
relation = "more than"
end
println("x is ", relation, " y")
end
toy (generic function with 1 method)
julia> toy(0, 0)
x is equal to y
julia> toy(9527, 1314)
x is more than y
julia> toy(1314, 9527)
x is less than y
其中relation
变量在if
块中定义,却在外部使用。
然而,当基于该特性时,要确保所有可能代码路径都为该变量定义了值。
下面改写的toy
函数将导致运行时错误:
julia> function toy(x, y)
if x < y
relation = "less than"
elseif x == y
relation = "equal to"
else
"more than"
end
println("x is ", relation, " y")
end
toy (generic function with 1 method)
julia> toy(0, 0)
x is equal to y
julia> toy(1314, 9527)
x is less than y
julia> toy(9527, 1314)
ERROR: UndefVarError: relation not defined
Stacktrace:
[1] toy(::Int64, ::Int64) at .\REPL[15]:9
[2] top-level scope at none:0
而且if
块还有返回值,这让来自别的编程语言的群众似乎不直观。该返回值就是简单地返回所选分支最后一条被计算的声明。
因此:
julia> huaan = 9527
9527
julia> if huaan > 1314
"positive"
else
"negative"
end
"positive"
注意Julia中非常短的条件声明(一行)频繁采用短路计算,正如后边概述的。
不同于C、MATLAB、Perl、Python和Ruby,但和Java、少数几个强类型编程语言相似,如果条件表达式是除true
和false
之外任何东西,都报错。
julia> if "qiuxiang"
println("bitch")
end
ERROR: TypeError: non-boolean (String) used in boolean context
Stacktrace:
[1] top-level scope at none:0
错误提示说条件是错误的String
类型,而非要求的Bool
类型。
所谓的三元操作符?:
和if-elseif-else
紧密相关,仅用于需要单个表达式值间的条件选择,和更长代码块的条件执行形成鲜明对比。因大多数编程语言中仅用三个被操作数的操作符而得名:condition ? one(true): another(false)
。
在?
之前的表达式condition
是条件表达式,如果条件表达式是真,三元操作符计算表达式one
(:
之前的),否则计算another
(:
之后的)。
牢记?
和:
前后的空白是必须的,像condition?one?another
不是合法的三元表达式(但?
和:
之后接受新行)。
理解这种行为最简单的方式就是看例子。在之前的示例中,三个分支共同调用println
函数:唯一真正的选项就是所打印的字面字符串。用三元操作符改写会更简明。为了清晰(for the sake of clarify),让咱一起先尝试个“双路”版本:
julia> x = 9527; y = 1314
1314
julia> println(x < y ? "less than" : "more than")
more than
julia> x, y = y, x
(1314, 9527)
julia> println(x < y ? "less than" : "more than")
less than
如果表达式x < y
是真,则整个三元操作符表达式计算字符串"less than"
,否则是"more than"
。
原始的“三路”例程需要链接多个三元操作符一起来:
julia> toy(x, y) = println(x < y ? "less than" : x > y ? "more than" : "equal to")
toy (generic function with 1 method)
julia> toy(9527, 1314)
more than
julia> toy(1314, 9527)
less than
julia> toy(0, 0)
equal to
为帮助(理解)链接,操作符从右到左关联。
值得注意的是,像if-elseif-else
一样,在:
之前或之后的表达式,仅在条件表达式是真或是假是相应地被计算。
julia> pseudo(what) = (print(what); what)
pseudo (generic function with 1 method)
julia> true ? pseudo("yes") : pseudo("no")
yes"yes"
julia> false ? pseudo("yes") : pseudo("no")
no"no"
短路计算和条件计算非常相似。这种行为在大多数命令式编程语言中也能找到有&&
和||
布尔操作符:一系列由这些操作符连接的布尔表达式,只计算足够判定整个链最终布尔值的最少数成员布尔表达式。摊开讲,意思就是:
- 在
one && another
表达式中,仅当表达式one
是true
时才计算表达式another
。 - 在
one || another
表达式中,仅当表达式one
是false
时才计算表达式another
。
原因是one && another
只要one
是假则整体必然是假,无论another
真假;同理one || another
只要one
是真则整体必然是真,无论another
真假。
且(&&
)和或(||
)都是从左到右关联,但且优先级高于或。
很同意试出且或的行为:
julia> mTrue(x) = (println(x); true)
mTrue (generic function with 1 method)
julia> mFalse(x) = (println(x); false)
mFalse (generic function with 1 method)
julia> mTrue(9527) && mTrue(1314)
9527
1314
true
julia> mTrue(9527) && mFalse(1314)
9527
1314
false
julia> mFalse(9527) && mTrue(1314)
9527
false
julia> mFalse(9527) && mFalse(1314)
9527
false
julia> mTrue(9527) || mTrue(1314)
9527
true
julia> mTrue(9527) || mFalse(1314)
9527
true
julia> mFalse(9527) || mTrue(1314)
9527
1314
true
julia> mFalse(9527) || mFalse(1314)
9527
1314
false
可以相同方式简单地试出复合且或操作符的结合性和优先级。
经常拿且或短路计算组成非常短的可选if
声明。
可以写<condition> && <statement>
(条件是然后声明)来替代if <condition> <statement> end
。
同理,可以写<condition> || <statement>
(条件否然后声明)来替代if ! <condition> <statement> end
。
举个例子,递归阶乘协程可定义如下:
julia> function mFactorial(n::Int)
n >= 0 || error("n must be non-negative")
n == 0 && return 1
n * mFactorial(n-1)
end
mFactorial (generic function with 1 method)
julia> mFactorial(9)
362880
julia> mFactorial(0)
1
julia> mFactorial(-1)
ERROR: n must be non-negative
Stacktrace:
[1] error(::String) at .\error.jl:33
[2] mFactorial(::Int64) at .\REPL[62]:2
[3] top-level scope at none:0
无短路计算的不二操作可通过【算术操作符和基本函数】中介绍的位布尔运算(&
和|
)实现。
这些(按位与或)是正常函数,恰好(happen to)支持中缀操作符语法,但总是计算各个参数(表达式):
julia> mFalse(9527) & mTrue(1314)
9527
1314
false
julia> mTrue(9527) & mTrue(1314)
9527
1314
true
正如if
、elseif
或三元操作符中使用的条件表达式,&&
或||
的被操作数必须是布尔类型。
除最后一个条目外,任何地方在条件链中使用非布尔类型值都是错误的:
julia> 9527 && true
ERROR: TypeError: non-boolean (Int64) used in boolean context
Stacktrace:
[1] top-level scope at none:0
另一方面,条件链末尾可以使用任何类型的表达式,将根据前一个条件的结果被计算、被返回。
julia> true && (9527, 1314)
(9527, 1314)
julia> false || (1314, 9527)
(1314, 9527)
有两种表达式重复计算的设计:while
循环、for
循环。
先上个while
循环的例子:
julia> i = 1
1
julia> while i<= 5
println(i)
global i+= 1
end
1
2
3
4
5
while
循环计算条件表达式(本例中是i <= 5
),只要是true
,持续计算循环体。
一旦条件表达式是false
,循环体不再被计算。
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
循环一个相当重要的区别就是在for
循环上下文中变量可见。
如果变量i
并非别的环境引入的,在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 k in 1:5
println(k)
end
1
2
3
4
5
julia> for role ∈ ["huaan", "qiuxiang"]
println(role)
end
huaan
qiuxiang
各种各样的可迭代容器类型将在手册的手续部分引入和讨论(查看【多维数组】)。
有些时候,在测试条件虚假(篡改)之前停止while
循环或在到达可迭代对象末尾之前停止for
循环是便捷的。
可以用break
关键字实现:
julia> i = 1
1
julia> while true
println(i)
if i >= 5
break
end
global i += 1
end
1
2
3
4
5
julia> for j in 1:9527
println(j)
if j >= 5
break
end
end
1
2
3
4
5
没有break
关键字,上述while
循环将永远不能自行退出,for
循环一直要迭代到9527。
这些循环都是用break
提前结束。
在别的环境中,能停止当前迭代并立即转移到下一个迭代是很方便的,这个用continue
关键字实现:
julia> for k = 1:9
if k % 3 != 0
continue
end
println(k)
end
3
6
9
这是几分做作的例子,因为将条件取反并将打印语句置于条件块中能产生同样行为。
实践中,在continue
之后有更多代码要计算,通常有多个调用continue
的点。
多个嵌套for
循环可以结合到单个外部循环,形成可迭代对象的笛卡尔积(cartesian product)。
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 = 9527
j = 1314
end
(1, 3)
(1, 4)
(2, 3)
(2, 4)
如果此例用for
关键字重写每个变量,输出将会不同(第二组和第四组会变)。
julia> for i = 1:2
for j = 3:4
println((i, j))
i = 9527
j = 1314
end
end
(1, 3)
(9527, 4)
(2, 3)
(9527, 4)
当不期望的情况(condition)发生了,函数也许不能给调用者返回合理的值。 在这种情况(case)下,针对异常情况(situation),最好要么打印诊断结论错误信息并终止程序,要么码农提供处理异常情况(circumstance)的代码允许采取恰当行动。
下表列出中断正常控制流的内建异常:
违例 | 描述 |
---|---|
ArgumentError |
函数参数类型不对。 |
BoundsError |
索引越界。 |
CompositeException |
|
DivideError |
整型除运算分母为零。 |
DomainError |
作用域(范围或上下文环境)错误。 |
EOFError |
文件或流没有更多可用数据。 |
ErrorException |
一般错误异常。 |
InexactError |
不精确转换错误。 |
InitError |
执行模块的__init__ 函数报错。 |
InterruptException |
终端CTRL+C 引发。 |
InvalidStateException |
找不到文档(TODO: No documentation found)。 |
KeyError |
字典或集合访问或删除不存在的元素。 |
LoadError |
发生在include 、require 、using 过程的错误。 |
OutOfMemoryError |
请求超出系统内存或垃圾回收容量。 |
ReadOnlyMemoryError |
写只读内存。 |
RemoteException |
来自Distributed ,远程计算捕捉、本地回放(远程捕捉为CapturedException 包装序列化远程调用栈)。 |
MethodError |
找不到方法。 |
OverflowError |
内存溢出。 |
Meta.ParseError |
无效的Julia表达式。 |
SystemError |
系统调用错误(Linux/Windows中进程退出的errno 不为零)。 |
TypeError |
类型断言错误或用不正确参数调用固有函数。 |
UndefRefError |
未定义的元素或字段。 |
UndefVarError |
未定义符号。 |
StringIndexError |
访字符串字节索引不是有效字符。 |
例如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:
[1] throw_complex_domainerror(::Symbol, ::Float64) at .\math.jl:31
[2] sqrt at .\math.jl:479 [inlined]
[3] sqrt(::Int64) at .\math.jl:505
[4] top-level scope at none:0
TODO: 说成“参数取值超出合法域”是可理解的,其实归到ArgumentError
更直观。
可自定义异常:
julia> struct MyCustomizedException <: Exception end
help?> MyCustomizedException
search: MyCustomizedException
No documentation found.
Summary
≡≡≡≡≡≡≡≡≡
struct MyCustomizedException <: Exception
Supertype Hierarchy
≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡
MyCustomizedException <: Exception <: Any
提前透露类的继承(派生)语法。
异常可用throw
显式创建。
例如定义了只允许非负参数的函数当碰到负数参数则throw
一个DomainError
异常。
julia> f(x) = x >= 0 ? exp(-x) : throw(DomainError(x, "argument must be nonnegative"))
f (generic function with 1 method)
julia> f(0)
1.0
julia> f(-1)
ERROR: DomainError with -1:
argument must be nonnegative
Stacktrace:
[1] f(::Int64) at .\REPL[43]:1
[2] top-level scope at none:0
要注意DomainError
没有圆括号的话不是异常实例,而是异常类型。
必须通过调用获得异常对象。
julia> typeof(DomainError(nothing)) <: Exception
true
julia> typeof(DomainError) <: Exception
false
透露了一种判断对象是某类型否的方法。
此外,有些异常类型接受一个或多个参数用于报告异常:
julia> throw(UndefVarError(:x))
ERROR: UndefVarError: x not defined
Stacktrace:
[1] top-level scope at none:0
该机制通过定制异常类型很容易实现,例如自定义的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"))
好。 - 有些时候,如果参数是大写的,异常信息保持第一个单词首字母大写就在理(make sense):
size(A,1) == size(B,2) || throw(DimensionMismatch("A has first dimension..."))
。
有个error
函数用来产生ErrorException
并中断正常控制流。
假设群众想立即停止给负数开平方根的执行,可以定义要求苛刻(挑剔)的sqrt
函数版本,如果参数是负数就抛出错误。
julia> sqrtfussy(x) = x >= 0 ? sqrt(x) : error("negative x not allowed")
sqrtfussy (generic function with 1 method)
julia> sqrtfussy(0)
0.0
julia> sqrtfussy(-1)
ERROR: negative x not allowed
Stacktrace:
[1] error(::String) at .\error.jl:33
[2] sqrtfussy(::Int64) at .\REPL[55]:1
[3] top-level scope at none:0
如果在别的函数中以负数调用sqrtfussy
则直接返回并在交互式会话中显示错误信息,而不是继续执行主调函数。
# 书接前文
julia> function fucksqrtfussy(x)
println("before sqrtfussy")
r = sqrtfussy(x)
println("after sqrtfussy")
end
fucksqrtfussy (generic function with 1 method)
julia> fucksqrtfussy(0)
before sqrtfussy
after sqrtfussy
julia> fucksqrtfussy(-1)
before sqrtfussy
ERROR: negative x not allowed
Stacktrace:
[1] error(::String) at .\error.jl:33
[2] sqrtfussy at .\REPL[55]:1 [inlined]
[3] fucksqrtfussy(::Int64) at .\REPL[58]:3
[4] top-level scope at none:0
话说try-catch
声明允许Exception
排雷。
举个栗子,客户自定义的开平方根函数用Exception
能自动调用实数或负数求平方根方法。
julia> f(x) = try
sqrt(x)
catch
sqrt(complex(x, 0))
end
f (generic function with 1 method)
julia> f(0)
0.0
julia> f(-1)
0.0 + 1.0im
重点注意真实代码实现上述逻辑,应该比较x
和零而不是捕捉异常(这一点和Python鼓励先尝试后道歉风格不同)。
异常比简单比较和分支慢得多。
而且try-catch
声明还允许Exception
保存为变量。
下面做作得栗子计算x
第二个元素的平方根(如果x
是支持索引访问的),否则假设x
是实数并返回其平方根。
julia> sqrtkiss(x) = try
sqrt(x[2])
catch y
if isa(y, DomainError)
println("convert to complex")
sqrt(complex(x[2], 0))
elseif isa(y, BoundsError)
println("guess single number")
sqrt(x)
end
end
sqrtkiss (generic function with 1 method)
julia> sqrtkiss(9527)
guess single number
97.60635225229964
julia> sqrtkiss([9527, 1314])
36.24913792078372
julia> typeof([9527])
Array{Int64,1}
julia> sqrtkiss([9527])
guess single number
ERROR: MethodError: no method matching sqrt(::Array{Int64,1})
Closest candidates are:
sqrt(::Float16) at math.jl:1004
sqrt(::Complex{Float16}) at math.jl:1005
sqrt(::Missing) at math.jl:1056
...
Stacktrace:
[1] sqrtkiss(::Array{Int64,1}) at .\REPL[70]:9
[2] top-level scope at none:0
julia> sqrtkiss(-9527)
guess single number
ERROR: DomainError with -9527.0:
sqrt will only return a complex result if called with a complex argument. Try sqrt(Complex(x)).
Stacktrace:
[1] throw_complex_domainerror(::Symbol, ::Float64) at .\math.jl:31
[2] sqrt at .\math.jl:479 [inlined]
[3] sqrt at .\math.jl:505 [inlined]
[4] sqrtkiss(::Int64) at .\REPL[70]:9
[5] top-level scope at none:0
注意紧随catch
的符号总是解释为异常的名称,所需单行书写try-catch
表达式要分外小心。
下面栗子不能正常工作,万一有错,返回x
的值。
julia> bomb() = throw(ErrorException("bomb"))
bomb (generic function with 1 method)
julia> x = 1314
1314
julia> try bomb() catch x end
julia> typeof(x)
Nothing
julia> try bomb() catch; x end
julia> typeof(x)
Nothing
在catch
后插入分号或换行:
# 书接前文
julia> try bomb()
catch
x
end
julia> typeof(x)
Nothing
其实try-catch
结构的力量在于能直接将嵌套计算展开为主调函数栈的更高层次。
也有没碰到任何错误的情况,但能合乎心意地展开栈并传递值到更高层次。
Julia提供rethrow
、backtrace
和catch_backtrace
函数做更高级地错误处理。
代码中执行状态变化或使用如文件等资源,通常需要在代码结束时清理工作(如关闭文件)。
异常潜在地让这个任务复杂化,因为异常可以引发代码块没有执行到正常结尾而退出。
而finally
关键字提供一种执行给定代码块后必然执行某些代码地方法,不论给定代码块如何退出的。
例如,下面演示确保文件关闭的方法:
julia> aFile = open("D:\\Important!\\happy\\howtojulia\\manual\\eventually-closed-open.out")
IOStream(<file D:\Important!\happy\howtojulia\manual\eventually-closed-open.out>)
julia> try
# do something
finally
close(aFile)
end
当控制流离开try
代码块(如由于return
或就是正常结束),则close(aFile)
将被执行。
如果try
代码块因为异常退出,异常将继续传播。
也可以给try-finally
结合catch
代码块,这种情况下finally
代码块将在catch
异常处理之后运行。
协程就是允许以灵活方式挂起和恢复计算。 这个特性有时也叫别的名字,如对称协程、轻量级线程、多任务协作或一习性续体(one-shot continuation)。
当计算工作(实践中就是运行特定函数)的一个片段设计为协程,就有可能被中断并切换到别的任务。原来的任务以后会恢复,从离开的点开始。 乍一看,这跟函数调用似乎相似。然而有两个关键区别: 一、协程切换不耗费任何空间,因此可以切换任意数量的协程二不耗费调用栈; 二、协程切换可以任何顺序发生,不同于函数调用,回到主调函数之前被调用函数必须结束执行。
这种控制流使得解决特定问题很容易。 在某些问题中,必须工作的各式片段并非由函数调用自然关联;在需要作得工作中没有明确的“调用者”和“被调用者”。 一个典型案例就是“生产者——消费者”问题,其中一个复杂过程生产值,另一个复杂过程消费值。 消费者不能简单调用生产者来获取一个值,因为生产者可能有很多值要生产,大概还没准备好返回。 利用协程,生产者和消费者都可以按需执行,按需传递和获取值。
Julia提供Channel
机制来解决协程问题。
一个Channel
就是一个可等待的先进先出(FIFO)队列,允许多个任务读出或写入该队列。
让咱定义一个生产者任务,通过调用put!
产出值。
为了消费值,需要调度生产者在新任务中运行。
特殊的Channel
结构接受单参数函数作为参数,用来运行绑定在Channel
上的任务。
然后就可以从Channel
对象重复take!
值。
julia> function producer(chn::Channel)
put!(chn, "start")
for n in 1:9
put!(chn, 9527n)
end
put!(chn, "stop")
end
producer (generic function with 1 method)
julia> chna = Channel(producer)
Channel{Any}(sz_max:0,sz_curr:1)
julia> take!(chna)
"start"
julia> take!(chna)
9527
julia> take!(chna)
19054
julia> take!(chna)
28581
julia> take!(chna)
38108
julia> take!(chna)
47635
julia> take!(chna)
57162
julia> take!(chna)
66689
julia> take!(chna)
76216
julia> take!(chna)
85743
julia> take!(chna)
"stop"
julia> take!(chna)
ERROR: InvalidStateException("Channel is closed.", :closed)
Stacktrace:
[1] check_channel_state at .\channels.jl:120 [inlined]
[2] take_unbuffered(::Channel{Any}) at .\channels.jl:318
[3] take!(::Channel{Any}) at .\channels.jl:306
[4] top-level scope at none:0
一种思考producer
行为的方式是它能返回多次。
在调用put!
之间,生产者的执行是挂起的,消费者掌控(此间局面)。
已经返回的Channel
可当作可迭代对象用于for
循环,循环变量逐次获取全部生产的值。
当通道(队列)关闭,循环终止。
julia> for x in Channel(producer)
println(x)
end
start
9527
19054
28581
38108
47635
57162
66689
76216
85743
stop
注意,不需要显式关闭生产者的通道。
因为绑定Channel
到协程的动作伴随所绑定任务的通道打开生命周期。
当任务结束,通道自动关闭。
多个通道可以绑定到一个任务,反之亦然(一个通道可被多个任务绑定)。
当Task
构造函数期望零参数函数,则Channel
方法创建一个通道、绑定到接受一个Channel
类型参数的函数。
生产者的一种通用模式是参数化,这种情况下,需要一个偏函数应用创建零参数或单参数【匿名函数】。
对于Task
对象,可以直接或用快捷宏操作完成:
julia> function taskLazy(one)
# do something
end
taskLazy (generic function with 1 method)
julia> taskHandle = Task(() -> taskLazy(9527))
Task (runnable) @0x000000000f86c010
julia> taskHandle_ = @task taskLazy(1314)
Task (runnable) @0x0000000010dd0b90
编排更多高级工作分布式模式,可以将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
:计算针对当前任务键值存储。
大多数任务切换的发生,是等待诸如IO请求等事件,并由Julia基础的调度器执行。 该调度器维护可执行任务队列,运行事件循环,基于诸如消息到达等外部事件重启任务。
基础的等待函数是wait
。
若干对象实现了wait
,例如,给定Process
对象,wait
将等其退出。
wait
通常是隐式的,可能发生在read
内部等待数据可用。
各种情况下wait
最终操作Condition
对象,它负责将任务排队并重启之。
当一个任务在Condition
对象上调用wait
,该任务被标记为不可运行,添加到该条件的队列,切换到调度器。
调度器紧接着挑选别的任务运行,或者阻塞等待外部事件。
如果一切顺利,最后一个事件处理函数会在Condition
对象上调用notify
,这让等待该条件的任务再次重新可循行。
调用Task
显式创建的协程最初调度器并不知道。
这允许用yieldto
管理任务。
然而,当这种任务等待事件时,当事件发生,仍然会自动如期重启。
也可以让调度器随时执行一个任务,不需要等待任何事件。
通过调用schedule
或用@async
宏实现(详见【并行计算】)。
协程有state
字段描述执行状态。
一个Task
的状态是下列符号之一:
符号 | 意义 |
---|---|
:runnable | 正在运行或可被切换到运行。 |
:waiting | 阻塞等待特定事件。 |
:queued | 在调度器的运行队列中,即将重启。 |
:done | 成功结束运行。 |
:failed | 以一个未捕捉到的异常结束。 |
- 协程的层面上和Go及大部分语言的新趋势相似。