Julia 控制流

2022-02-25 16:47 更新

Julia 提供一系列控制流:

前五個(gè)控制流機(jī)制是高級(jí)編程語言的標(biāo)準(zhǔn)。但任務(wù)不是:它提供了非本地的控制流,便于在臨時(shí)暫停的計(jì)算中進(jìn)行切換。在 Julia 中,異常處理和協(xié)同多任務(wù)都是使用的這個(gè)機(jī)制。

復(fù)合表達(dá)式

用一個(gè)表達(dá)式按照順序?qū)σ幌盗凶颖磉_(dá)式求值,并返回最后一個(gè)子表達(dá)式的值,有兩種方法:begin 塊和 (;) 鏈。 begin 塊的例子:

julia> z = begin
         x = 1
         y = 2
         x + y
       end
3

這個(gè)塊很短也很簡單,可以用 (;) 鏈語法將其放在一行上:

julia> z = (x = 1; y = 2; x + y)
3

這個(gè)語法在函數(shù)中的單行函數(shù)定義非常有用。 begin 塊也可以寫成單行, (;) 鏈也可以寫成多行:

julia> begin x = 1; y = 2; x + y end
3

julia> (x = 1;
        y = 2;
        x + y)
3

條件求值

一個(gè) if-elseif-else 條件表達(dá)式的例子:

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

如果條件表達(dá)式 x < y 為真,相應(yīng)的語句塊將會(huì)被執(zhí)行;否則就執(zhí)行條件表達(dá)式 x > y ,如果結(jié)果為真,相應(yīng)的語句塊將被執(zhí)行;如果兩個(gè)表達(dá)式都是假,else 語句塊將被執(zhí)行。這是它用在實(shí)際中的例子:

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 塊是可選的。

請(qǐng)注意,非常短的條件語句(一行)在 Julia 中是會(huì)經(jīng)常使用短的電路評(píng)估(Short-Circuit Evaluation)實(shí)現(xiàn)的,具體細(xì)節(jié)在下一節(jié)中進(jìn)行概述。

如果條件表達(dá)式的值是除 truefalse 之外的值,會(huì)出錯(cuò):

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

“問號(hào)表達(dá)式”語法 ?:if-elseif-else 語法相關(guān),但是適用于單個(gè)表達(dá)式:

a ? b : c

? 之前的 a 是條件表達(dá)式,如果為 true ,就執(zhí)行 : 之前的 b 表達(dá)式,如果為 false ,就執(zhí)行 :c 表達(dá)式。

用問號(hào)表達(dá)式來重寫,可以使前面的例子更加緊湊。先看一個(gè)二選一的例子:

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

三選一的例子需要鏈?zhǔn)秸{(diào)用問號(hào)表達(dá)式:

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

鏈?zhǔn)絾柼?hào)表達(dá)式的結(jié)合規(guī)則是從右到左。

if-elseif-else 類似,: 前后的表達(dá)式,只有在對(duì)應(yīng)條件表達(dá)式為 truefalse 時(shí)才執(zhí)行:

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"

短路求值

&&|| 布爾運(yùn)算符被稱為短路求值,它們連接一系列布爾表達(dá)式,僅計(jì)算最少的表達(dá)式來確定整個(gè)鏈的布爾值。這意味著: 在表達(dá)式 a && b 中,只有 atrue 時(shí)才計(jì)算子表達(dá)式 b 在表達(dá)式 a || b 中,只有 afalse 時(shí)才計(jì)算子表達(dá)式 b &&|| 都與右側(cè)結(jié)合,但 &&|| 優(yōu)先級(jí)高:

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 里經(jīng)常作為 if 語句的一個(gè)簡潔的替代。 可以把 if <cond> <statement> end 寫成 <cond> && <statement> (讀作 <cond> *從而* <statement>)。 類似地, 可以把 if ! <cond> <statement> end 寫成 <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

短路求值運(yùn)算符,可以使用數(shù)學(xué)運(yùn)算和基本函數(shù)中介紹的位布爾運(yùn)算符 &|

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

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

&&|| 的運(yùn)算對(duì)象也必須是布爾值( truefalse )。在任何地方使用一個(gè)非布爾值,除非最后一個(gè)進(jìn)入連鎖條件的是一個(gè)錯(cuò)誤:

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

另一方面,任何類型的表達(dá)式可以使用在一個(gè)條件鏈的末端。根據(jù)前面的條件,它將被評(píng)估和返回:

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

重復(fù)求值: 循環(huán)

有兩種循環(huán)表達(dá)式: while 循環(huán)和 for 循環(huán)。下面是 while 的例子:

julia> i = 1;

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

上例也可以重寫為 for 循環(huán):

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

此處的 1:5 是一個(gè) Range 對(duì)象,表示的是 1, 2, 3, 4, 5 序列。 for 循環(huán)遍歷這些數(shù),將其逐一賦給變量 i 。 while 循環(huán)和 for 循環(huán)的另一區(qū)別是變量的作用域。如果在其它作用域中沒有引入變量 i ,那么它僅存在于 for 循環(huán)中。不難驗(yàn)證:

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

julia> j
ERROR: j not defined

有關(guān)變量作用域,詳見變量的作用域

通常,for 循環(huán)可以遍歷任意容器。這時(shí),應(yīng)使用另一個(gè)(但是完全等價(jià)的)關(guān)鍵詞 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

手冊中將介紹各種可迭代容器(詳見多維數(shù)組)。

有時(shí)要提前終止 whilefor 循環(huán)??梢酝ㄟ^關(guān)鍵詞 break 來實(shí)現(xiàn):

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

有時(shí)需要中斷本次循環(huán),進(jìn)行下一次循環(huán),這時(shí)可以用關(guān)鍵字 continue

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

多層 for 循環(huán)可以被重寫為一個(gè)外層循環(huán),迭代類似于笛卡爾乘積的形式:

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

這種情況下用 break 可以直接跳出所有循環(huán)。

異常處理

當(dāng)遇到意外條件時(shí),函數(shù)可能無法給調(diào)用者返回一個(gè)合理值。這時(shí),要么終止程序,打印診斷錯(cuò)誤信息;要么程序員編寫異常處理。

內(nèi)置異常 Exception

如果程序遇到意外條件,異常將會(huì)被拋出。表中列出內(nèi)置異常。

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

例如,當(dāng)對(duì)負(fù)實(shí)數(shù)使用內(nèi)置的 sqrt 函數(shù)時(shí),將拋出 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 函數(shù)

可以使用 throw 函數(shù)顯式創(chuàng)建異常。例如,某個(gè)函數(shù)只對(duì)非負(fù)數(shù)做了定義,如果參數(shù)為負(fù)數(shù),可以拋出 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 使用時(shí)需要使用帶括號(hào)的形式,否則返回的并不是異常,而是異常的類型。必須帶括號(hào)才能返回 Exception 對(duì)象:

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

julia> typeof(DomainError) <: Exception
false

另外,一些異常類型使用一個(gè)或更多個(gè)參數(shù)用來報(bào)告錯(cuò)誤:

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

這個(gè)機(jī)制能被簡單實(shí)現(xiàn),通過按照下列所示的 UndefVarError 方法自定義異常類型:

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

error 函數(shù)

error 函數(shù)用來產(chǎn)生 ErrorException ,阻斷程序的正常執(zhí)行。

如下改寫 sqrt 函數(shù),當(dāng)參數(shù)為負(fù)數(shù)時(shí),提示錯(cuò)誤,立即停止執(zhí)行:

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

當(dāng)對(duì)負(fù)數(shù)調(diào)用 fussy_sqrt 時(shí),它會(huì)立即返回,顯示錯(cuò)誤信息:

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 函數(shù)

Julia 還提供一些函數(shù),用來向標(biāo)準(zhǔn)錯(cuò)誤 I/O 輸出一些消息,但不拋出異常,因而并不會(huì)打斷程序的執(zhí)行:

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 語句可以用于處理一部分預(yù)料中的異常 Exception 。例如,下面求平方根函數(shù)可以正確處理實(shí)數(shù)或者復(fù)數(shù):

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

但是處理異常比正常采用分支來處理,會(huì)慢得多。

try/catch 語句使用時(shí)也可以把異常賦值給某個(gè)變量。例如:

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 之后的符號(hào)會(huì)被解釋為一個(gè)異常的名稱,因此,需要注意的是,在單行中寫 try/catch 表達(dá)式時(shí)。下面的代碼將不會(huì)正常工作返回 x 的值為了防止發(fā)生錯(cuò)誤:

try bad() catch x end

我們在 catch 后使用分號(hào)或插入換行來實(shí)現(xiàn):

try bad() catch; x end

try bad()
catch
  x
end

Julia 還提供了更高級(jí)的異常處理函數(shù) rethrowbacktracecatch_backtrace 。

finally 語句

在改變狀態(tài)或者使用文件等資源時(shí),通常需要在操作執(zhí)行完成時(shí)做清理工作(比如關(guān)閉文件)。異常的存在使得這樣的任務(wù)變得復(fù)雜,因?yàn)楫惓?huì)導(dǎo)致程序提前退出。關(guān)鍵字 finally 可以解決這樣的問題,無論程序是怎樣退出的,finally 語句總是會(huì)被執(zhí)行。

例如, 下面的程序說明了怎樣保證打開的文件總是會(huì)被關(guān)閉:

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

當(dāng)程序執(zhí)行完 try 語句塊(例如因?yàn)閳?zhí)行到 return 語句,或者只是正常完成),close 語句將會(huì)被執(zhí)行。如果 try 語句塊因?yàn)楫惓L崆巴顺?,異常將?huì)繼續(xù)傳播。catch 語句可以和 tryfinally 一起使用。這時(shí)。finally 語句將會(huì)在 catch 處理完異常之后執(zhí)行。

任務(wù)(也稱為協(xié)程)

任務(wù)是一種允許計(jì)算靈活地掛起和恢復(fù)的控制流,有時(shí)也被稱為對(duì)稱協(xié)程、輕量級(jí)線程、協(xié)同多任務(wù)等。

如果一個(gè)計(jì)算(比如運(yùn)行一個(gè)函數(shù))被設(shè)計(jì)為 Task,有可能因?yàn)榍袚Q到其它 Task 而被中斷。原先的 Task 在以后恢復(fù)時(shí),會(huì)從原先中斷的地方繼續(xù)工作。切換任務(wù)不需要任何空間,同時(shí)可以有任意數(shù)量的任務(wù)切換,而不需要考慮堆棧問題。任務(wù)切換與函數(shù)調(diào)用不同,可以按照任何順序來進(jìn)行。

任務(wù)比較適合生產(chǎn)者-消費(fèi)者模式,一個(gè)過程用來生產(chǎn)值,另一個(gè)用來消費(fèi)值。消費(fèi)者不能簡單的調(diào)用生產(chǎn)者來得到值,因?yàn)閮烧叩膱?zhí)行時(shí)間不一定協(xié)同。在任務(wù)中,兩者則可以正常運(yùn)行。

Julia 提供了 produceconsume 函數(shù)來解決這個(gè)問題。生產(chǎn)者調(diào)用 produce 函數(shù)來生產(chǎn)值:

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

要消費(fèi)生產(chǎn)的值,先對(duì)生產(chǎn)者調(diào)用 Task 函數(shù),然后對(duì)返回的對(duì)象重復(fù)調(diào)用 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 循環(huán)中迭代任務(wù),生產(chǎn)的值被賦值給循環(huán)變量:

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

注意 Task() 函數(shù)的參數(shù),應(yīng)為零參函數(shù)。生產(chǎn)者常常是參數(shù)化的,因此需要為其構(gòu)造零參匿名函數(shù) ??梢灾苯訉?,也可以調(diào)用宏:

function mytask(myarg)
    ...
end

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

produceconsume 但它并不在不同的 CPU 發(fā)起線程。我們將在并行計(jì)算中,討論真正的內(nèi)核線程。

核心任務(wù)操作

盡管 produceconsume 已經(jīng)闡釋了任務(wù)的本質(zhì),但是他們實(shí)際上是由庫函數(shù)調(diào)用更原始的函數(shù) yieldto 實(shí)現(xiàn)的。 yieldto(task,value) 掛起當(dāng)前任務(wù),切換到特定的 task , 并使這個(gè) task 的最后一次 yeidlto 返回 \特定的 value。注意 yieldto 是唯一需要的操作來進(jìn)行 ‘任務(wù)風(fēng)格’的控制流;不需要調(diào)用和返回,我們只用在不同的任務(wù)之間切換即可。 這就是為什么這個(gè)特性被稱做 “對(duì)稱式協(xié)程”;每一個(gè)任務(wù)的切換都是用相同的機(jī)制。

yeildto 很強(qiáng)大, 但是大多數(shù)時(shí)候并不直接調(diào)用它。 當(dāng)你從當(dāng)前的任務(wù)切換走,你有可能會(huì)想切換回來, 但需要知道切換的時(shí)機(jī)和任務(wù),這會(huì)需要相當(dāng)?shù)膮f(xié)調(diào)。 例如,procude 需要保持某個(gè)狀態(tài)來記錄消費(fèi)者。無需手動(dòng)地記錄正在消費(fèi)的任務(wù)讓 produceyieldto 更容易使用。

除此之外,為了高效地使用任務(wù),其他一些基本的函數(shù)也同樣必須。current_task() 獲得當(dāng)前運(yùn)行任務(wù)的引用。istaskdone(t) 查詢?nèi)蝿?wù)是否終止。istaskstarted(t) 查詢?nèi)蝿?wù)是否啟動(dòng)。task_local_storage 處理當(dāng)前任務(wù)的鍵值儲(chǔ)存。

任務(wù)與事件

大多數(shù)任務(wù)的切換都是在等待像 I/O 請(qǐng)求這樣的事件的時(shí)候,并由標(biāo)準(zhǔn)庫的調(diào)度器完成。調(diào)度器記錄正在運(yùn)行的任務(wù)的隊(duì)列,并執(zhí)行一個(gè)循環(huán)來根據(jù)外部事件(比如消息到達(dá))重啟任務(wù)。

處理等待事件的基本函數(shù)是 wait。 有幾種對(duì)象實(shí)現(xiàn)了 wait,比如對(duì)于 Process 對(duì)象, wait 會(huì)等待它終止。更多的時(shí)候 wait 是隱式的,比如 wait 可以發(fā)生在調(diào)用 read 的時(shí)候,等待數(shù)據(jù)變得可用。

在所有的情況中, wait 最終會(huì)操作在一個(gè)負(fù)責(zé)將任務(wù)排隊(duì)和重啟的 Condition 對(duì)象上。當(dāng)任務(wù)在 Condition 上調(diào)用 wait, 任務(wù)會(huì)被標(biāo)記為不可運(yùn)行,被加入到 Condition 的 隊(duì)列中,再切換至調(diào)度器。調(diào)度器會(huì)選取另一個(gè)任務(wù)來運(yùn)行,或者等待外部事件。如果一切正常,最終一個(gè)事件句柄會(huì)在 Condition 上調(diào)用 notify,使正在等待的任務(wù)變得可以運(yùn)行。

調(diào)用 Task 可以生成一個(gè)初始對(duì)調(diào)度器還未知的任務(wù),這允許你用 yieldto 手動(dòng)管理任務(wù)。不管怎樣,當(dāng)這樣的任務(wù)正在等待事件時(shí),事件一旦發(fā)生,它仍然會(huì)自動(dòng)重啟。而且任何時(shí)候你都可以調(diào)用 schedule(task) 或者用宏 @schedule@async 來讓調(diào)度器來運(yùn)行一個(gè)任務(wù),根本不用去等待任何事件。(參見并行計(jì)算)

任務(wù)狀態(tài)

任務(wù)包含一個(gè) state 域,它用來描述任務(wù)的執(zhí)行狀態(tài)。任務(wù)狀態(tài)取如下的幾種符號(hào)中的一種:

符號(hào) 意義
:runnable 任務(wù)正在運(yùn)行,或可被切換到該任務(wù)
:waiting 等待一個(gè)特定事件從而阻塞
:queued 在調(diào)度程序的運(yùn)行隊(duì)列中準(zhǔn)備重新啟動(dòng)
:done 成功執(zhí)行完畢
:failed 由于未處理的異常而終止


以上內(nèi)容是否對(duì)您有幫助:
在線筆記
App下載
App下載

掃描二維碼

下載編程獅App

公眾號(hào)
微信公眾號(hào)

編程獅公眾號(hào)