Erlang極簡(jiǎn)學(xué)習(xí)筆記<09>——進(jìn)程篇

  • Erlang的并發(fā)是基于消息傳遞和Actor模型的

  • 在Erlang中,并發(fā)(Concurrncy)指的是有許多獨(dú)立運(yùn)行的actor,但是并不要求它們同時(shí)運(yùn)行,而并行(Parallelism)指的是多個(gè)actor在同時(shí)運(yùn)行

  • Erlang對(duì)可靠性要求很高,因此采用了一種最徹底的做法,禁止進(jìn)程之間共享內(nèi)存

  • 因?yàn)樵诔霈F(xiàn)崩潰之后,共享內(nèi)存會(huì)導(dǎo)致系統(tǒng)中的狀態(tài)不一致,使問(wèn)題復(fù)雜化

  • 與共享內(nèi)存的方式不同,進(jìn)程之間只能通過(guò)發(fā)送消息進(jìn)行通信,所有的消息數(shù)據(jù)都是復(fù)制的。這種方式效率會(huì)低一點(diǎn),但是更安全

  • 當(dāng)系統(tǒng)中的某個(gè)部分出現(xiàn)了錯(cuò)誤,造成了數(shù)據(jù)破壞,那么這個(gè)部分應(yīng)該盡快死亡以防止錯(cuò)誤和壞數(shù)據(jù)傳播到系統(tǒng)的剩余部分

  • Erlang通過(guò)在VM中實(shí)現(xiàn)進(jìn)程,這樣實(shí)現(xiàn)者們可以對(duì)優(yōu)化和可靠性進(jìn)行完全掌控

  • 一個(gè)Erlang進(jìn)程大概占用300個(gè)字的內(nèi)存空間,創(chuàng)建時(shí)間只有幾微妙

  • 為了管理程序所創(chuàng)建的所有進(jìn)程,VM會(huì)為每個(gè)核啟動(dòng)一個(gè)線程來(lái)充當(dāng)一個(gè)調(diào)度器(scheduler)

  • 每個(gè)調(diào)度器有一個(gè)運(yùn)行隊(duì)列(run queue),也就是一個(gè)Erlang進(jìn)程列表,會(huì)給其中的每個(gè)進(jìn)程分配一小段運(yùn)行時(shí)間片

  • 當(dāng)某個(gè)調(diào)度器的運(yùn)行隊(duì)列中任務(wù)過(guò)多時(shí),會(huì)把一部分任務(wù)遷移到其他隊(duì)列中。這意味著,每個(gè)Erlang VM都會(huì)進(jìn)行負(fù)載均衡操作,程序員無(wú)需關(guān)心

  • Erlang并發(fā)編程需要3個(gè)原語(yǔ):創(chuàng)建(spawn)進(jìn)程、發(fā)送消息及接收消息

  • 在Erlang中進(jìn)程就是一個(gè)函數(shù)。進(jìn)程運(yùn)行一個(gè)函數(shù),一般運(yùn)行結(jié)束,進(jìn)程就消失了

  • 要啟動(dòng)一個(gè)新進(jìn)程,可以使用Erlang提供的函數(shù)spawn/1,這個(gè)函數(shù)以一個(gè)函數(shù)為參數(shù),并運(yùn)行它

    > F = fun() -> 2 + 2 end.
    > spawn(F).
    <0.82.0>
    
  • spawn/1的返回值(<0.82.0>)稱為進(jìn)程標(biāo)識(shí)符,通常寫(xiě)成pid、PidPID

  • pid是一個(gè)隨意設(shè)定的值,用來(lái)表示虛擬機(jī)運(yùn)行期間的某個(gè)時(shí)間點(diǎn)上存在(或曾經(jīng)存在)的某個(gè)進(jìn)程

  • 可以用pid作為地址進(jìn)行進(jìn)程間的通信

  • 在上面的例子中,我們無(wú)法得到函數(shù)F的返回值。我們只能得到它的pid。因?yàn)檫M(jìn)程不會(huì)返回任何東西

  • 使用BIF的self/0函數(shù),可以返回當(dāng)前進(jìn)程的pid

  • Erlang的消息傳遞原語(yǔ)——操作符!,也稱為bang符號(hào)。該操作符的左邊是一個(gè)pid,右邊可以是任意Erlang數(shù)據(jù)項(xiàng)。這個(gè)數(shù)據(jù)項(xiàng)會(huì)被發(fā)送給左邊的pid所代表的進(jìn)程,這個(gè)進(jìn)程就可以訪問(wèn)它了

    > self() ! hello.
    
  • 消息會(huì)被放到接收進(jìn)程的郵箱中,但是并沒(méi)有被讀取。上面例子中出現(xiàn)的第二個(gè)hello是這個(gè)發(fā)送函數(shù)的返回值。這意味著,可以用如下方式給多個(gè)進(jìn)程發(fā)送同樣的消息

    > self() ! self() ! double
    
  • 進(jìn)程郵箱中的消息是按照接收順序保存的,每當(dāng)讀取一個(gè)消息時(shí),就會(huì)把消息從郵箱中取出

    > flush().
    Shell got hello
    Shell got double
    Shell got double
    ok
    
  • flush/0函數(shù)只是一種輸出所收到的消息的快捷方法

  • 使用receive表達(dá)式來(lái)接收消息。receive的語(yǔ)法和case...of非常相似。事實(shí)上,它們的模式匹配部分的工作原理完全一樣,只是receive模式中變量會(huì)綁定到收到的消息,而不是caseof之間的表達(dá)式。receive表達(dá)式也可以有衛(wèi)語(yǔ)句

    receive
        Pattern1 when Guard1 -> Expr1;
        Pattern2 when Guard2 -> Expr2;
        Pattern3 -> Expr3
    end
    
  • 要想知道進(jìn)程是否收到了消息,唯一的方法是讓它發(fā)送一條回應(yīng)。我們的進(jìn)程如果需要知道要把回應(yīng)發(fā)送給誰(shuí),就必須在消息中添加我們的pid

  • 在Erlang中,我們通過(guò)把進(jìn)程pid打包在一個(gè)元組中完成這項(xiàng)工作,如果不這樣做,那么消息就都是匿名的。打包的結(jié)果是一條類似{Pid, Message}的消息

  • 我們來(lái)編寫(xiě)一個(gè)海豚程序來(lái)展示消息的收發(fā)

    -module(dolphins).
    -compile(export_all).
    
    dolphin() ->
        receive
            {From, do_a_flip} ->
                From ! "How about no?",
                dolphin();
            {From, fish} ->
                From ! "So long and thanks for all the fish!";
            _ ->
                io:format("Heh, we're smarter than you humans.~n"),
                dolphin()
        end.
    
    Eshell
    > Dolphin = spawn(dolphins, dolphin, []).
      <0.85.0>
    > Dolphin ! {self(), do_a_flip}.
      {<0.78.0>,do_a_flip}
    > Dolphin ! {self(), unknown_message}.
      Heh, we're smarter than you humans.
      {<0.78.0>,unknown_message}
    > Dolphin ! {self(), fish}.
      {<0.78.0>,fish}
    > flush().
      Shell got "How about no?"
      Shell got "So long and thanks for all the fish!"
      ok
    
  • 在上面的測(cè)試中,引入了一個(gè)新的進(jìn)程創(chuàng)建函數(shù)spawn/3。不再只以一個(gè)函數(shù)為參數(shù),spawn/3函數(shù)有3個(gè)參數(shù):模塊、函數(shù)、和函數(shù)參數(shù)

  • 如果進(jìn)程和actor只是一些能收發(fā)消息的函數(shù),并不會(huì)帶來(lái)多少好處。為了能夠得到更大的好處,需要在進(jìn)程中持有狀態(tài)

  • 借助于遞歸函數(shù)的幫助,進(jìn)程的狀態(tài)可以全部存放到遞歸函數(shù)的參數(shù)中

  • 如果直接使用消息的收發(fā),程序員則需要知道每個(gè)進(jìn)程自身使用的協(xié)議。這是一個(gè)無(wú)意義的負(fù)擔(dān)。

  • 一種好的方式是,使用函數(shù)來(lái)處理消息的接收和發(fā)送,從而把消息隱藏起來(lái)

    store(Pid, Food) ->
        Pid ! {self(), {store, Food}},
        receive
            {Pid, Msg} -> Msg
        end.
    
  • 同樣Erlang中也習(xí)慣在模塊中,增加一個(gè)start/1函數(shù)來(lái)隱藏進(jìn)程啟動(dòng)

    start() ->
        spawn(?MODULE, dolphin, []).
    
  • ?MODULE是一個(gè)宏,它的值是當(dāng)前模塊的名字

  • receive可以使用after子句來(lái)處理超時(shí)

    receive
        Match -> Expression1
    after Delay ->
        Expression2
    end
    
  • 當(dāng)過(guò)了Delay(單位:毫秒)時(shí)間后,還沒(méi)有收到和Match模式相匹配的消息,就會(huì)執(zhí)行after部分

  • 實(shí)際上after除了可以接收毫秒值外,還可以接收原子infinity

  • 在大多數(shù)語(yǔ)言中,異常都是使用try...catch這種方式在程序執(zhí)行流內(nèi)處理的

  • 這種常見(jiàn)的做法存在一個(gè)問(wèn)題,要么必須在正常代碼邏輯的每一層中處理異常錯(cuò)誤,要么只好把錯(cuò)誤處理的負(fù)擔(dān)一直推到程序的最頂層中處理。這樣做雖然可以捕獲所有的錯(cuò)誤,但卻再也無(wú)法知道錯(cuò)誤出現(xiàn)的原因了

  • Erlang除了支持常見(jiàn)的異常處理模式,還支持另一種層次的異常處理。可以把異常處理邏輯從程序的正常執(zhí)行流中移出來(lái),放到另外一個(gè)并發(fā)進(jìn)程中。這種方法會(huì)讓代碼更加整潔,只用考慮那些“正常的情況”

  • 鏈接(link)是兩個(gè)進(jìn)程之間的一種特殊關(guān)系。當(dāng)兩個(gè)進(jìn)程間建立了這種關(guān)系后,如果其中一個(gè)進(jìn)程由于意外的拋出、出錯(cuò)或者退出而死亡時(shí),另外一個(gè)進(jìn)程也會(huì)死亡,把這兩個(gè)進(jìn)程獨(dú)立的生存期綁定成一個(gè)關(guān)聯(lián)在一起的生存期

  • 從盡快失敗阻止錯(cuò)誤蔓延的角度來(lái)說(shuō),這是一個(gè)非常有用的概念。如果某個(gè)進(jìn)程由于錯(cuò)誤崩潰了,但依賴于它的進(jìn)程卻繼續(xù)運(yùn)行,那么所有這些依賴進(jìn)程都必須要處理依賴缺失情況。讓它們死亡,然后重啟整個(gè)進(jìn)程組通常是一種可以接受的替代方案。鏈接就是實(shí)現(xiàn)這種功能的

  • Erlang中又一個(gè)原生函數(shù)link/1,用于在兩個(gè)進(jìn)程間建立一條鏈接,它的參數(shù)是進(jìn)程的pid。當(dāng)調(diào)用它時(shí),會(huì)在當(dāng)前進(jìn)程和參數(shù)pid標(biāo)識(shí)的進(jìn)程之間建立一條鏈接。要去除鏈接可以使用unlink/1

  • 當(dāng)鏈接進(jìn)程中的一個(gè)死亡時(shí),會(huì)發(fā)送一條特殊的消息,其中含有死亡原因相關(guān)的信息。如果進(jìn)程正常死亡了(函數(shù)執(zhí)行完畢),就不會(huì)發(fā)送這條消息

    -module(linkmon).
    -compile(export_all).
    
    myproc() ->
        timer:sleep(5000),
        exit(reason).
    
    Eshell
    > c(linkmon).
    > spawn(fun linkmon:myproc/0).
    > link(spawn(fun linkmon:myproc/0)).
      true
      ** exception error: reason
    
  • 注意!鏈接不會(huì)堆疊,如果在同樣的兩個(gè)進(jìn)程之間調(diào)用了多次link/1,那么這兩個(gè)進(jìn)程之間只會(huì)存在一條鏈接,只需一次unlink/1調(diào)用就可以解除這個(gè)鏈接

  • link(spawn(Function))或者link(spawn(M, F, A))并不是一個(gè)原子操作。有時(shí)進(jìn)程會(huì)在鏈接建立成功之前死亡,從而導(dǎo)致不期望的行為。因此,Erlang中增加了spawn_link/1-3函數(shù)。這個(gè)函數(shù)的參數(shù)和spawn/1-3完全一樣,創(chuàng)建一個(gè)進(jìn)程,并和它建立鏈接,就像使用了link/1一樣,不過(guò)這是一個(gè)原子調(diào)用(兩個(gè)操作被合并成一個(gè)操作,要么成功,要么失敗,不會(huì)出現(xiàn)其他情況)

    > spawn_link(fun linkmon:myproc/0).
      <0.90.0>
      ** exception error: reason
    
  • 跨進(jìn)程的錯(cuò)誤傳播對(duì)進(jìn)程來(lái)說(shuō)和消息傳遞類似,不過(guò)使用的是一種稱為信號(hào)(signal)的特殊消息。退出信號(hào)是一種“秘密”消息,會(huì)自動(dòng)作用到進(jìn)程上并殺死它們

  • 鏈接可以完成快速殺死進(jìn)程的工作,還缺少快速重啟部分。要重啟一個(gè)進(jìn)程,首先需要知道它已經(jīng)死亡了,有一種稱為系統(tǒng)進(jìn)程的概念,可以完成這項(xiàng)工作

  • 系統(tǒng)進(jìn)程就是一般的進(jìn)程,只是它們可以把退出信號(hào)轉(zhuǎn)換成普通的消息。進(jìn)程可以通過(guò)調(diào)用process_flag(trap_exit, true)實(shí)現(xiàn)這一點(diǎn)

    > process_flag(trap_exit, true).
    > spawn_link(fun linkmon:myproc/0).
    > receive X -> X end.
      {'EXIT',<0.97.0>,reason}
    
  • 也許殺死進(jìn)程并不是你想要的,也許你只想當(dāng)一個(gè)跟蹤者。如果是這樣,那么監(jiān)視器(monitor)可能就是你想要的

  • 監(jiān)控器是一種特殊類型的鏈接

  • 監(jiān)控器是單向的

  • 在兩個(gè)進(jìn)程之間可以設(shè)置多個(gè)監(jiān)控器(監(jiān)控器可以疊加,每個(gè)監(jiān)控器有自己的標(biāo)識(shí))

  • 如果一個(gè)進(jìn)程想知道另外一個(gè)進(jìn)程的死活,但是這兩個(gè)進(jìn)程之間并沒(méi)有強(qiáng)的業(yè)務(wù)關(guān)聯(lián)時(shí),可以使用監(jiān)視器

  • 創(chuàng)建監(jiān)控器的函數(shù)是erlang:monitor/2,它的第一個(gè)參數(shù)永遠(yuǎn)是原子process,第二個(gè)參數(shù)是pid

    > erlang:monitor(process, spawn(fun() -> timer:sleep(500) end)).
    > flush().
      Shell got {'DOWN',#Ref<0.4159903409.3575906310.207444>,process,<0.80.0>,normal}
    
  • 每當(dāng)被監(jiān)控的進(jìn)程死亡時(shí),監(jiān)控進(jìn)程都會(huì)收到一條消息,格式是{'DOWN', MonitorReference, process, Pid, Reason}。其中的引用可以用來(lái)解除對(duì)一個(gè)進(jìn)程的監(jiān)控

  • 記住!監(jiān)控器是可以疊加的,因此會(huì)收到多條DOWN消息。引用可以唯一確定一條DOWN消息

  • 和鏈接一樣,監(jiān)控器也有一個(gè)原子性質(zhì)的函數(shù),可以在創(chuàng)建進(jìn)程的同時(shí)監(jiān)控它:spawn_monitor/1-3

    > {Pid, Ref} = spawn_monitor(fun() -> receive _ -> exit(boom) end end).
    > erlang:demonitor(Ref).
    > Pid ! dir.
    > flush().
    
  • 這個(gè)例子我們?cè)谶M(jìn)程死亡前解除了對(duì)它的監(jiān)控,因此無(wú)法跟蹤到它的死亡消息。還有另一個(gè)函數(shù)demonitor/2,它的功能會(huì)多一點(diǎn)。第二個(gè)參數(shù)是一個(gè)選項(xiàng)列表。不過(guò),只有兩個(gè)可用選項(xiàng):infoflush

    > erlang:demonitor(Ref, [flush, info]).
      false
    
  • info選項(xiàng)用來(lái)指示某個(gè)監(jiān)控器在解除時(shí)是否還存在。這也是為何這里調(diào)用返回了false

  • flush選項(xiàng)會(huì)把郵箱中存在的DOWN消息都清除掉

  • Erlang還為進(jìn)程提供了一個(gè)命名的方法。通過(guò)給進(jìn)程起一個(gè)名字,可以用一個(gè)原子而不是一個(gè)不可理解的pid來(lái)標(biāo)識(shí)一個(gè)進(jìn)程??梢允褂眠@個(gè)原子名給進(jìn)程發(fā)送消息,和pid完全一樣

  • 可以使用函數(shù)erlang:register(Name, Pid)為進(jìn)程命名。如果進(jìn)程死亡,它會(huì)自動(dòng)失去自己的名字。也可以使用函數(shù)unregister/1手工解除進(jìn)程的名字注冊(cè)

  • 可以調(diào)用registered/0得到所有已注冊(cè)進(jìn)程的列表,或者通過(guò)Eshell命令regs()得到更詳細(xì)的信息

  • 通過(guò)函數(shù)whereis/1可以獲取已注冊(cè)進(jìn)程的pid

  • 如果有一個(gè)數(shù)據(jù)可以被多個(gè)進(jìn)程看到,這就是大家熟知的共享狀態(tài)

  • 如果多個(gè)不同進(jìn)程同時(shí)訪問(wèn)數(shù)據(jù)、修改數(shù)據(jù)的內(nèi)容,導(dǎo)致信息不一致,發(fā)生軟件錯(cuò)誤。對(duì)這種情況有一個(gè)常用術(shù)語(yǔ):競(jìng)爭(zhēng)條件(race condition)

  • 競(jìng)爭(zhēng)條件非常危險(xiǎn),因?yàn)樗鼈兊某霈F(xiàn)依賴于事件的時(shí)序。在幾乎所有現(xiàn)存的并發(fā)和并行語(yǔ)言中,這種時(shí)序都和一些不可預(yù)測(cè)的因素有關(guān),如處理器的繁忙程度、進(jìn)程運(yùn)行的位置以及程序所處理的數(shù)據(jù)類型

  • 在實(shí)際使用Erlang收發(fā)消息時(shí),我們應(yīng)該通過(guò)引用(make_ref())來(lái)作為識(shí)別消息的唯一值,并用它來(lái)保證從正確的進(jìn)程收到了正確的消息

    judge2(Band, Album) ->
        Ref = make_ref(),
        critic ! {self(), Ref, {Band, Album}},
        receive
            {Ref, Criticism} -> Criticism
        after 2000 ->
            timeout
        end.
    
    critic2() ->
        receive
            {From, Ref, {_Band, _Album}} ->
                From ! {Ref, "They are terrible!"}
        end,
        critic2().
    
  • 最后請(qǐng)記住,原子的個(gè)數(shù)是有限的。絕對(duì)不要?jiǎng)討B(tài)創(chuàng)建原子。這意味著,命名進(jìn)程應(yīng)該保留給那些單個(gè)VM實(shí)例中唯一的、重要的并且在整個(gè)應(yīng)用運(yùn)行期間都要一直存在的服務(wù)。如果需要為那些暫時(shí)的或者VM中并不唯一的進(jìn)程命名,就意味著可能需要把它們看成一個(gè)群組。明智的做法是把它們鏈接在一起,讓它們共存亡,而不是試圖使用動(dòng)態(tài)的名字

原文地址

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • erlang常規(guī)面試題 基礎(chǔ) 消息發(fā)送 基礎(chǔ)相關(guān) OTP相關(guān) gen_server:cast和erlang:sen...
    randyjia閱讀 4,379評(píng)論 1 3
  • Erlang是一門(mén)函數(shù)式編程語(yǔ)言。Erlang的核心特征是容錯(cuò),并發(fā)只是容錯(cuò)這個(gè)約束下的一個(gè)副產(chǎn)品 對(duì)于同樣的參數(shù)...
    shixiongfei閱讀 751評(píng)論 0 0
  • Windows環(huán)境下安裝Erlang ??在http://www.erlang.org/downloads下載安裝...
    驍兵閱讀 2,324評(píng)論 0 3
  • 并發(fā) 創(chuàng)建進(jìn)程 使用 erlang:spawn/1,2,3,4 用來(lái)創(chuàng)建一個(gè) erlang 進(jìn)程。Erlang 進(jìn)...
    Shawn_xiaoyu閱讀 14,059評(píng)論 9 22
  • 多進(jìn)程 Elixir強(qiáng)大的并發(fā)來(lái)自其actor并發(fā)模型,簡(jiǎn)而言之就是可以使用大量的進(jìn)程來(lái)實(shí)現(xiàn)并發(fā)。elixir中的...
    人世間閱讀 2,551評(píng)論 5 7

友情鏈接更多精彩內(nèi)容