跳到主要內容

Erlang 123

小山 (ArcBlock 后端工程师)
Erlang

作者 小山(ArcBlock 後端工程師)

之前在程序人生公眾號撰寫過一篇小文Erlang入坑指南,文中以6個Erlang函數介紹了一下Erlang的世界觀。在ArcBlock,我們的後端服務用Elixir打造。既然都用Elixir了,自然會涉及Erlang。這篇部落格文章是基於我們的Technical Learning Series技術培訓,用3個Erlang函數帶您認識Erlang的世界觀。

我們的世界

我們所生活的世界是一個並行的世界。人類總體構成一個社會,同時我們每個人也是獨立的個體。每個個體都有差異,我們都有自己獨立的思想,並且我和你之間不會共享記憶。舉個例子,雖然新郎新娘一起經歷了結婚這件事,但是在新郎的記憶裡面存著的是新郎的角度經歷的婚禮,新娘的記憶存的是以新娘的角度經歷的婚禮。新郎新娘各有一個婚禮的記憶副本,但是他們之間卻不共享任何記憶。

同時,人與人之間的交流完全是非同步進行的。我給你發了個微信之後會繼續滑我的朋友圈,你收到訊息也不一定立即回覆,訊息的傳遞是完全非同步進行的。所謂的同步訊息傳遞,比如打電話,也只是我們人為地約定俗成你說一句話,然後我說一句話來進行交流。我完全可以不等你說話而自顧自地說,這時候你會覺得我沒有禮貌,但是訊息傳遞的本質其實還是非同步傳遞。

程式語言

自從有了電腦,我們有了程式語言,目的是為了讓我們可以透過程式語言來告訴電腦做什麼。一開始我們用組合語言直接告訴電腦做什麼,但是這很麻煩,所以我們發明了高階程式語言,用我們人類能理解的方式寫程式碼,然後編譯器翻譯成機器語言去執行相應的命令。我們在學校所接觸到的大部分高階語言都是物件導向的,物件導向的語言嘗試用物件來模擬我們的真實世界,物件源於類別。比如,桌上的水杯是一個水杯類別的實體,該類別有屬性比如水杯容量,該類別有方法比如接水倒水。這是很不錯的模擬現實世界的方式,但是物件導向語言最大的問題就是找不到物件。

Erlang則嘗試從另一個角度來模擬我們的真實世界,那就是用行程。每個行程都是獨立個體,有自己的狀態,互相之間不共享任何資訊,行程之間唯一的通訊手段就是非同步訊息傳遞。在Erlang的世界裡面沒有類別和物件,有的只是一個個行程。Erlang的這種對世界的模擬方法和我們平時所生活的真實世界其實很像,所以理解Erlang的世界觀反而會更加自然直觀。接下來我們看看Erlang的世界觀用程式碼寫出來是什麼樣的。

hello world

先看最簡單的hello world長什麼樣。

-module(foo).
-export([hello/0]).

hello() ->
    io:format("world~n").

在終端機執行一下

$ erl
1> c(foo),foo:hello().    % c(foo)用來對foo模組進行編譯生成目標檔案.beam
                          % 每次進行改動後都要重新編譯載入最新的目標檔案
world
ok

接下來我們在呼叫io:format/1之前加一個等待函數等待一秒鐘

hello() ->
    timer:sleep(1000),    % 1000毫秒為1秒鐘
    io:format("world~n").

再次在終端機呼叫

2> c(foo),foo:hello().
world                     % 1秒後列印出來
ok

如果我們呼叫3次,則會花3秒鐘

3> foo:hello(),foo:hello(),foo:hello().
world                     % 1秒後列印出來
world                     % 2秒後列印出來
world                     % 3秒後列印出來
ok

spawn

現在我們來看今天要講的第一個函數spawn/3。這個函數會啟動一個行程,該行程會執行一個函數。

4> spawn(foo,hello,[]).   % 傳入的三個參數分別是模組名稱、函數名稱和函數參數
                          % hello函數沒有參數,所以第三個參數我們傳入[]表示空
<0.105.0>                 % 返回的這個東西叫pid,表示行程的id(process id)
world                     % 1秒後列印出來

如果我們呼叫三次,看看會發生什麼

5> spawn(foo,hello,[]),spawn(foo,hello,[]),spawn(foo,hello,[]).
<0.111.0>                 % 返回一個pid
world
world
world                     % 1秒後同時列印出來3個訊息

也就是說,我們透過呼叫spawn/3三次開啟了三個行程,每個行程等待一秒後列印訊息,所以三條訊息同時被列印出來。這就是並行。

所以在Erlang裡面並行的概念極其簡單,就是呼叫spawn函數即可。要同時有100個行程並行做事,只需呼叫spawn100次。簡單粗暴。

send

一個行程被啟動後會有一個pid,我們可以透過該pid往行程發訊息。這就好像我們可以透過電話號碼給某人打電話一樣。事實上當我們啟動一個Erlang shell的時候,shell本身也是一個行程,我們可以給自己發條訊息。

6> self() ! 1.                        % self()會返回自己的pid,用驚嘆號表示發送訊息
1
7> process_info(self(),messages).     % 我們可以檢查一下自己的信箱裡面的訊息
{messages,[1]}                        % 可以看到剛才發送的1已經在郵件裡面了

以上就是非同步發送,是Erlang裡面行程交流的唯一手段。

receive

如何收取在信箱中的訊息呢?用receive函數

8> receive Msg -> Msg end.
1

好了,以上就是今天要講的3個函數。雖然很簡單,但是卻建構出了Erlang的世界觀:該世界由行程組成,行程之間不共享任何資訊,行程之間靠非同步訊息傳遞作為唯一通訊手段。

Erlang能做什麼?

知道這三個函數了,接下來呢?Erlang能用來做什麼呢?我們把剛才的hello/0函數稍作修改如下:

hello() ->
    receive
        {Msg,From} ->                            % 對於任何長成{Msg,From}的訊息
            io:format("~p: ~p~n",[From,Msg]),    % 我們將其列印出來
            hello()
    end.

我們重新開一個終端機,這次帶點參數

$ erl -sname a                                   % -sname 可以起個名字
(a@sbs)1> c(foo),P = spawn(foo,hello,[]).        % sbs是我的機器名稱,a是這個Erlang節點的名字
<0.107.0>                                        % 他們共同組成我這個Erlang節點的名字a@sbs
(a@sbs)2> register(a,P).                         % 我們為其註冊一個名字a
true
(a@sbs)3> a ! {1,node()}.                        % 我們可以給a這個名字發訊息, node()會返回我們節點名字
a@sbs: 1                                         % 當行程收到該訊息時將其列印出來
{1,a@sbs}

再開一個終端機,起名為b

$ erl -sname b
(b@sbs)1> a ! {1,node()}                         % 我們往a這個名字發條訊息
** exception error: bad argument                 % 結果出錯,因為在b@sbs這個節點中並沒有叫a的行程
     in operator  !/2
        called as a ! {1,b@sbs}
(b@sbs)4> {a,a@sbs} ! {1,node()}.                % 因為叫a的行程在a@sbs裡面,所以我們試著往a@sbs
{1,b@sbs}                                        % 節點中發訊息

這時我們看到在b節點中,訊息並沒有被列印出來,但是我們再看一下a節點的終端機視窗

b@sbs: 1                                         % 訊息在a的終端機視窗被列印出來了
(a@sbs)4>

短短幾行程式碼,我們實現了一個最簡單的聊天程式。原理其實非常簡單,就是往某個節點的某個行程發訊息,該行程收到訊息後將其列印。Erlang的簡潔之處在於,從語言層面來說,開發者無需關心底層的通訊協定,只需關心把什麼訊息發送到什麼地方,以及收到訊息後要如何處理該訊息。

和本文配套的培訓影片可在這裡找到。有興趣用Erlang/Elixir打造區塊鏈專案的夥伴們,請加入ArcBlock吧。