Skip to content

Latest commit

 

History

History
954 lines (785 loc) · 30.1 KB

控制流.md

File metadata and controls

954 lines (785 loc) · 30.1 KB

Julia提供各种各样的控制流概念:

  • 复合表达式:begin(;)
  • 条件计算:if-elseif-else?:(三元操作符)。
  • 短路计算:&&||和链式比较。
  • 重复计算(循环):whilefor
  • 异常处理:try-catch-finallyerrorthrow
  • 任务(即协程):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 < ytrue(成立),则相应的代码块被计算;否则条件表达式x > y被计算且如果是true(成立),则相应的代码块被计算;若前两个条件都不成立,则else代码块被执行。

其中elseifelse块是可选的,并且许多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、少数几个强类型编程语言相似,如果条件表达式是除truefalse之外任何东西,都报错。

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表达式中,仅当表达式onetrue时才计算表达式another
  • one || another表达式中,仅当表达式onefalse时才计算表达式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

正如ifelseif或三元操作符中使用的条件表达式,&&||的被操作数必须是布尔类型。 除最后一个条目外,任何地方在条件链中使用非布尔类型值都是错误的:

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循环迭代这些值,每轮分配给变量ifor循环和之前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声明退出整个嵌套循环,不仅是退出内部循环。 变量ij每次内部循环运行都设置为当前迭代值。 这样给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 发生在includerequireusing过程的错误。
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-finally

话说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提供rethrowbacktracecatch_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

编排更多高级工作分布式模式,可以将bindschedule结合TaskChannel构造函数来显式连接一组通道和一组“生产者——消费者”任务。

目前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及大部分语言的新趋势相似。