Erlang 123

作者 小山(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秒後列印出來
okspawn
現在我們來看今天要講的第一個函數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的簡潔之處在於,從語言層面來說,開發者無需關心底層的通訊協定,只需關心把什麼訊息發送到什麼地方,以及收到訊息後要如何處理該訊息。