2014/05/26

erlang - edoc

對於一個 erlang library 來說,因為 erlang 習慣把 client 跟 server 使用的 code 都放進同一個 module 裡面,而且程式裡面只能利用 export 的宣告,來知道這個 module 提供什麼 API 介面,至於要怎麼使用,就只能靠文件說明來輔助。

edoc 是 erlang 程式的 document generator,它是因應 Java 語言裡 Javadoc 的概念啟發,語法跟 javadoc 類似。

多國語言

因為 erlang R17 才預設 erl 原始程式碼都要使用 utf8 編碼,以 R16 的狀況,我們必須根據 Using Unicode in Erlang 文件的說明,因為 R16B 預設coding 為 bytewise (or latin1) encoding,如果在 module 的程式碼裡面要寫上中文字,而檔案要存成 utf-8 時,erl module source file 的第一行就必須要寫上

%% -*- coding: utf-8 -*-

如果沒有加上這個,產生 edoc 的文件就會直接變成亂碼。

edoc 實例分析

我們以 rabbitmq erlang client 為例,直接看一個正式的 erlang library 是怎麼使用 edoc。

首先,要先下載原始程式碼,通常我們會直接在官方網站 下載 原始程式碼,目前是 rabbitmq-server-3.2.4.tar.gz,但是下載後,查閱程式碼,Makefile 裡面只有很簡單的編譯語法,因此,我們就改變一下,到 rabbitmq-erlang-client github 下載程式碼,這裡的 Makefile 就有比較完整的 edoc 處理的資訊。

關於文件的部份,首先要修改 Makefile 前面的 VERSION 變數,原本是 0.0.0,但應該修改成 3.2.4。

# Makefile

VERSION=3.2.4

###############################################################################
##  Documentation
###############################################################################

documentation: $(DOC_DIR)/index.html

$(DOC_DIR)/overview.edoc: $(SOURCE_DIR)/overview.edoc.in
    mkdir -p $(DOC_DIR)
    sed -e 's:%%VERSION%%:$(VERSION):g' < $< > $@

$(DOC_DIR)/index.html: $(DEPS_DIR)/$(COMMON_PACKAGE_DIR) $(DOC_DIR)/overview.edoc $(SOURCES)
    $(LIBS_PATH) erl -noshell -eval 'edoc:application(amqp_client, ".", [{preprocess, true}, {macros, [{edoc, true}]}])' -run init stop

相關的變數是放在 common.mk 裡面,並直接由 Makefile 引用

# common.mk

SOURCE_DIR=src
DIST_DIR=dist
DEPS_DIR=deps
DOC_DIR=doc
......

先直接執行看看,直接 make 或 make all 都沒有看到 doc 文件的結果,所以要再 make documentation

> make documentation
mkdir -p doc
sed -e 's:%%VERSION%%:3.2.4:g' < src/overview.edoc.in > doc/overview.edoc
ERL_LIBS=deps:dist erl -noshell -eval 'edoc:application(amqp_client, ".", [{preprocess, true}, {macros, [{edoc, true}]}])' -run init stop

第一步是建立 doc 目錄
第二步是把 src/overview.edoc.in 移動到 doc/overview.edoc,同時修改版本號碼的內容
第三步是使用 edoc module 裡的 application,ERL_LIBS是 erl 接受的環境變數值,內容是erl執行時,所需要的其他 library 的目錄。接下來就 -eval 一段 erlang srcipt,最後再執行 init module 的 stop method 把erl關掉。

ERL_LIBS=deps:dist erl 的寫法,是因為 bash IFS 預設值為 space, a tab and a newline,寫在同一行,可以讓 erl 使用一個暫時的環境變數 ERL_LIBS,卻又不會永久影響該環境變數。

別的執行方式

> erl -noshell -run edoc_run packages '[""]' '[{dir, "doc"},{source_path, ["src"]}]'

在 Windows 因為參數 parsing 的問題,參數兩頭必須都要用雙引號,因此上面的指令要調成

erl -noshell -run edoc_run packages "[\"\"]" "[{dir, \"doc\"},{source_path, [\"src\"]}]"

在 linux 環境,單引號或雙引號的寫法都可以運作。

也可以這樣執行:

erl -noshell -run edoc_run application "'rabbitmq erlang client'" "\".\"" "[{def,{vsn,\"3.2.4\"}},{source_path, [\"src\"]}]"

edoc module

edoc_run 裡面有把如何從 erlang startup 的時候,就直接呼叫 edoc。但因為 rabbitmq 例子是從 shell 執行一段 script,所以要直接看 edoc 的文件,script是執行 edoc:applation/3 這個 method。

application(Application::atom(), Dir::filename(), Options::proplist()) -> ok

文件會被輸出到第二個參數目錄下的 doc 子目錄。preprocess 設定可讓所有 source files 都先讓 erlang preprocessor (epp) 先處理一次,當程式裡面使用了 macro 的時候,就一定要打開 preprocess 設定。同時要在 macros 列出所需要的 macro definitions,也就是要使用 edoc 裡面定義的巨集。

amqp_rpc_client.erl

amqp_rpc_cient edoc 是 edoc 的頁面,每個文件都有三個部份 (1) Description (2) Function Index (3) Function Details

對應到 amqp_rpc_client.erl 原始程式碼,最前面是版權宣告,在一行空白之後,就是 %% @doc 開頭的一段文字,這段文字的第一句話,會自動變成文件一開始的簡介,然後這一整段文字,都會成為 (1) Description 的內容。

接下來是 API 的部份,amqp_rpc_client 有三個 public function,每個function都有 %% @spec 與 %% @doc 兩段內容,這兩個部份會成為文件的 (3) Function Details 的段落。

而(2) Function Index可看到三個 public function 的 table。

後面的 function 都沒有 @spec 與 @doc 的說明,甚至到了 callback function ,還標記了 %% @private,這表示這些 function 屬於 library 內部使用,使用者不需要在文件上知道這些 function 的說明。

%% The contents of this file are subject to the Mozilla Public License
%% Version 1.1 (the "License"); you may not use this file except in
%% compliance with the License. You may obtain a copy of the License at
%% http://www.mozilla.org/MPL/
%% ...

%% @doc This module allows the simple execution of an asynchronous RPC over
%% AMQP. It frees a client programmer of the necessary having to AMQP
%% plumbing. .....
-module(amqp_rpc_client).

...

%%--------------------------------------------------------------------------
%% API
%%--------------------------------------------------------------------------

%% @spec (Connection, Queue) -> RpcClient
%% where
%%      Connection = pid()
%%      Queue = binary()
%%      RpcClient = pid()
%% @doc Starts a new RPC client instance that sends requests to a
%% specified queue. This function returns the pid of the RPC client process
%% that can be used to invoke RPCs and stop the client.
start(Connection, Queue) ->
    {ok, Pid} = gen_server:start(?MODULE, [Connection, Queue], []),
    Pid.

...

%%--------------------------------------------------------------------------
%% gen_server callbacks
%%--------------------------------------------------------------------------

%% Sets up a reply queue and consumer within an existing channel
%% @private
init([Connection, RoutingKey]) ->
....

edoc tags

在 edoc 可使用的完整 tag 列表,可在 edoc user's guide 裡面看到,tags 有分 generic, overview, module, function 四種,文件中也刻意把最複雜的 @spec @type 獨立一個章節 Type specifications。

running edoc

有四種

  1. edoc:application/2 產生一個 application 的文件
  2. edoc:packages/2 產生多個 packages 的文件
  3. edoc:files/2 產生數個 source files 的文件
  4. edoc:run/3 上面三個 functions 的最一般化的函數版本

overview page

在文件的目的目錄放置一個 overview.edoc 檔案,內容可以使用 Overview 與 Generic tags

Generic tags

可用在任何一個地方

  1. @clear
    會把前面所有 tags 都丟棄
  2. @docfile
    Reads a plain documentation file
  3. @end
    標記這是上一個 tag 的結尾
  4. @headerfile
    類似 @dodfile,通常是讀取 heade files: .hrl 檔案
  5. @todo 或 @TODO
    內容是 XHTML text,描述還沒有做完的工作
  6. @type
    a type declaration or definition

Overview tags

文件的目的目錄的 overview.edoc 檔案裡面可使用這些標籤

  1. @author
  2. @copyright
  3. @doc
  4. @reference
  5. @see
  6. @since
  7. @title
  8. @version

以 amqp erlang client 為例,overview.edoc 內容為

@title AMQP Client for Erlang
@author GoPivotal Inc. <support@rabbitmq.com>
@copyright 2007-2013 GoPivotal, Inc.

@version 3.2.4

@reference <a href="http://www.rabbitmq.com/protocol.html" target="_top">AMQP documentation</a> on the RabbitMQ website.

@doc

== Overview ==
....

%% ```f(X) ->
%%       case X of
%%          ...
%%       end'''

== Overview == 是 h3 的縮寫,就像是 Markdown 一樣

   == Heading ==
   === Sub-heading ===
   ==== Sub-sub-heading ====

另外這個語法

 ```...'''

會自動變成 pre,以免跟 xml tag 有衝突

<pre><![CDATA[...]]></pre>

Module tags

這些用在 module 宣告中,有很多標籤都跟 Overview tags 一樣

  1. @author
  2. @copyright
  3. @deprecated
  4. @doc
  5. @hidden
    不會出現在文件中,通常是 sample code, test modules
  6. @private
    標記這是 private module
  7. @reference
  8. @see
    refernce to a module, function, datatype, or application
  9. @since
  10. @version

Function tags

  1. @deprecated
  2. @doc
  3. @equiv
    等同於另一個 function call/expression
  4. @hidden
  5. @private
  6. @see
  7. @since
  8. @spec
    指定 function type
  9. @throws
  10. @type

函數規格 @spec

在 function 中,參數與回傳值的資料型別並不是清楚地寫在 function 的定義上,我們需要一個方式,來告訴使用這個函數的progarmmer,該怎麼使用它。erlang 社群開發了一種記號法,但這個記號法並不是 erlang 程式碼的一部分,而只是一種寫文件的工具。

這個記號法只能用在文件上,在程式碼中,會用 %% 將該行視為註解。通常會這樣寫 %% @spec

-module(math)
-export([fac/1]).

%% @spec fac(int()) -> int().

fac(0) -> 1;
fac(N) -> N * fac(N-1).

在使用此型別記號法時,要定義兩件事:型別與函數的規格。

定義型別

名稱為 typeName 的型別會寫成 typeName()

內建已經預先定義的型別是:

  1. any(): 指任何 erlang 的資料型別,term() 是 any() 的別名
  2. atom(), binary(), float(), function(), integer(), pid(), port(), reference(): erlang 的基本資料型別
  3. bool(): atom(true 或 false)
  4. char(): integer() 的子集合,代表字元
  5. iolist(): 遞迴地定義為 [char() | binary() | iolist()],通常用來產生高效率的字元輸出
  6. tuple()
  7. list(L): 是 [L] 的別名
  8. nil(): 就是 []
  9. string(): list(char()) 的別名
  10. depp_string(): 遞迴地定義為 [char()|deep_string()]
  11. none(): 沒有資料型別,用在不會產生回傳值的函數,例如無窮的接收迴圈,表示此函數不會返回

使用者自己定義型別可以寫成

@type newType() = TypeExpression

範例
@type onOff() = on|off.
@type person() = {person, name(), age()}.
@type people() = [person()].
@type name() = {firstname, string()}.
@type age() = integer().

指定函數的輸入與輸出型別

寫法為
@spec fuinctionName(T1, T2, ..., Tn) -> Tret
T1, T2, ..., Tn 是 參數的型別, Tret 是回傳值的資料型別

每個 T 都有三種可能的形式

  1. TypeVar
    型別變數,這代表未知型別(跟 erlang 的變數無關)
  2. TypeVar::Type
    型別變數後面跟著一個型別
  3. Type
    型別表示式

範例

@spec file:open(FileName, Mode) -> {ok, Handle} | {error, Why}.
@spec file:read_line(Handle) -> {ok, Line} | eof.

file:open/2 意思是,要開啟 FileName,會取得回傳值 {ok, Handle} 或是 {error, Why}
FileName 跟 Mode 是型別變數,但我們不知道它確切的型別是什麼。

範例

@spec lists:map(fun(A)->B, [A]) -> [B].
@spec lists:filter(fun(X) -> bool(), [X]) -> [X].

範例

@spec file:open(FileName::string(), [mode()]) -> {ok, Handle::file_handle()} | {error, Why::string()}.
@type mode() = read|write|compressed|raw|binary| ...

範例

@spec file:open(string(), Modes) -> {ok, Handle} | {error, string()}
    Handle() = file_handle(),
    Modes = [Mode],
    Mode = read|write|compressed|raw|binary| ...

範例

@spec file:open(string(), [mode()]) -> {ok,file_handle()} | error().
@type error() = {error, string()}.
@type mode() = read|write|compressed|raw|binary| ...

where

也可以用變數,搭配 where 來寫函數的定義

Spec ::= FunType "where"? DefList? | FunctionName FunType "where"? DefList?

範例

%% @spec (Connection, Queue) -> RpcClient
%% where
%%      Connection = pid()
%%      Queue = binary()
%%      RpcClient = pid()

上面的範例寫法,是先填上變數名稱,第二行之後,再加上 where 來更明確地限制每一個變數實際上的資料型別。

結論

對於 erlang 來說,applcation 的標準文件 edoc 是很重要的,我們必須透過 edoc 才能知道怎麼使用這個 library。

2014/05/25

在iOS應用程式中判斷使用者是否正在撥打電話 - 使用CoreTelephony framework的CTCallCenter

前言

當手機App正在前景執行時,總難避免正好有人打了一通電話過來。因此,手機App是否能在當下執行適當的處理,像是:主動幫使用者儲存編輯到一半的資料、暫時中斷已經進行到一半的VOIP通話...等等,是每個手機應用程式開發者都應該審慎面對的課題。
而在iOS系統中,當某個App正在前景執行時遇到了一通來電,則該應用程式會進入Inactive狀態
(關於應用程式的狀態,可參考我先前寫的一篇文章:淺談iOS應用程式背景執行(一) - 在有限時間內在背景執行任意工作)
然而這樣或許不夠;iOS上的手機App在前景執行時,有許多狀況都會使手機App進入Inactive狀態,你也許會想要明確地區分出,什麼時候才是遇到了一通來電。
iOS系統提供了CoreTelephony framework,能夠輕易地幫iOS應用程式開發者處理這個問題。

使用CoreTelephony framework的CTCallCenter

CTCallCenter類別除了能夠取得當前手機的通話狀態,也能夠註冊特定的事件處理函式,讓應用程式在通話狀態變更時,執行特定的程式碼。
CTCallCenter類別位於CoreTelephony framework。在開始使用CTCallCenter類別之前,務必記得將之加入專案中。
之後,在程式中import需要用到的類別即可。

取得當前通話狀態

以下範例是取得當前使用者是否正在進行通話:
#import <CoreTelephony/CTCallCenter.h>
#import <CoreTelephony/CTCall.h>

CTCallCenter *callCenter = [[CTCallCenter alloc] init];
for (CTCall *call in callCenter.currentCalls)  {
    if (call.callState == CTCallStateConnected) {
        //目前有通話正在進行中
    }
}
CTCallCenter的 currentCalls property表示當前的所有通話。
為一個由CTCall物件所組成的陣列。
而一個CTCall物件實例表示了一通電話。
CTCall類別有兩個property: callIDcallState
callID是這通通話的唯一識別碼,callState則是這通通話目前的狀態。兩者皆為NSString*型態。
其中callState可能的內容如下:
1. CTCallStateDialing:撥號中。
2. CTCallStateIncoming:有來電。
3. CTCallStateConnected:正在通話中。
4. CTCallStateDisconnected:通話節數。

為通話狀態變更時,註冊事件處理函式

除了在程式中主動偵測目前的通話狀態之外,
為通話狀態變更時,註冊事件處理函式,
讓程式能在通話狀態變更時,執行特定的處理,
往往更為實用,範例如下:
#import <CoreTelephony/CTCallCenter.h>
#import <CoreTelephony/CTCall.h>

@implementation SomeViewController {
    CTCallCenter *callCenter;
}

- (void)viewDidLoad
{
    self.callCenter = [[CTCallCenter alloc] init];
    self.callCenter.callEventHandler = ^(CTCall* ctcall) {
        // do something, for example ...
        if (call.callState == CTCallStateConnected) {
            //目前有通話正在進行中
        }
    };
}
以上範例,能在Callback函式中取得通話狀態發生變化的CTCall物件,
此時,再由該物件取得通話狀態,並進行對應的處理即可。

結語

撰寫手機App,與PC應用程式上的及網頁應用程式,
最大的不同,莫過於,除了有限的硬體設備外,還需要處理手機App執行時遇到一通來電的狀況。

在iOS系統上有簡潔易用的CoreTelephony framework可以幫助iOS App開發者處理這個狀況,務必花上一點時間學習了解如何使用。

2014/05/23

圖片的base64編碼與解碼

client要將檔案以http protocol傳上server有許多方式,除了一般普遍使用的multipart/form-data外本文介紹將圖檔以base64編成字串後再傳遞的方式。

base64編碼與解碼需借助apache common codec library

圖片轉成byte需借助ImageIO

程式碼如下

import java.awt.image.BufferedImage;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.IOException;

import javax.imageio.ImageIO;

import org.apache.commons.codec.binary.Base64;

public class TestBase64 {
    public static void main(String[] args) {
        try {
            // 將檔案透過imageio轉成byte
            String filename = "/Users/james/Downloads/IMAGE_1.jpg";
            BufferedImage img = ImageIO.read(new File(filename));
            ByteArrayOutputStream bos = new ByteArrayOutputStream();

            ImageIO.write(img, "jpg", bos);
            byte[] imageBytes = bos.toByteArray();
            bos.close();

            // 將byte執行編碼
            String imageString = Base64.encodeBase64String(imageBytes);

            // 印出編碼結果
            System.out.println("length=" + imageString.length());
            System.out.println("string=" + imageString);

            // 再將編碼後的字串轉成圖檔
            String filename_2 = "/Users/james/Downloads/IMAGE_2.jpg";
            BufferedImage bufferedImage = null;

            byte[] imageByte = Base64.decodeBase64(imageString);
            ByteArrayInputStream bis = new ByteArrayInputStream(imageByte);
            bufferedImage = ImageIO.read(bis);
            bis.close();

            ImageIO.write(bufferedImage, "jpg", new File(filename_2));
        } catch (IOException e) {
            e.printStackTrace();
        }

    }
}

經過測試後原本IMAGE_1的圖檔size為1.1MB,經過BASE64編解碼、ImageIO讀寫後IMAGE_2的size居然縮小為440k。經過進一步比較後,後者的檔案exif會遺失,圖片品質的話看不出有明顯的改變。檔案變小的問題應該是在ImageIO。如果要維持原檔不變的話可試試別種libray將圖檔轉成byte。

2014/05/19

erlang - multicore

對 erlang 來說,不需要修改程式,就可以運作在多核新的 CPU 上,提昇執行速度。唯一的條件是,必須要以 spawn 多個行程的方式實作程式,不能用巨大的序列程式實作,序列化程式必須要改寫成行程的方式。

如何讓程式在多核心 CPU 上執行地更快

原則如下

  1. 使用多個行程
    要讓 CPU 保持忙碌,唯一的方式就是使用多個行程

  2. 避免副作用 side effect
    具有共享記憶體,兩個thread可同時寫入相同記憶體位置的共時系統,會利用上鎖的方式來保護寫入的區域,一般是由程式語言在內部以 mutex 或同步化的方式來實現 lock。
    erlang 不具有共享記憶體,不存在這個問題,共享資料可透過 ETS/DETS

    "public" ETS table 可被多個行程共享,只要知道 table 識別字的行程都可以讀寫此 table,這相當危險,所以 programmer 必須注意程式邏輯,加上使用的限制:
    2.1 同一時間只有一個行程會去寫入資料,其他行程只會讀取資料
    2.2 負責寫入 ETS table 的行程永遠要正確,不能寫入錯誤的資料

    "protected" ETS table 比較安全,只有一個行程可以寫入 table,其他行程可讀取資料。

    ETS/DETS 的目的是用來實現 Mnesia,如果要在行程之間共享記憶體,應用程式應該使用 Mnesia 的 transaction,盡可能不要獨立使用 ETS/DETS。

  3. 避免序列化瓶頸
    序列化瓶頸就是數個共時行程需要存取序列資源,最常見的序列化瓶頸是 disk IO,這是無法避免的。

    每一次產生了「註冊行程」,就會產生一個潛在的序列瓶頸,盡量避免使用「註冊行程」,如果使用「註冊行程」實作 server,必須確定它會很快地回應 request。

    解決序列瓶頸的唯一方法就是改變演算法,改成分散式演算法,這在網路程式或多核心程式常常會用到。

    分散式訂票系統:如果要賣票,通常會用單一售票處處理所有票務,但這會產生序列瓶頸,解決方式就是開設兩個售票處,第一個售票處分配偶數號,第二個售票處分配奇數號,如果售票處1賣完了,再把第二個售票處的票移到第一個賣。雖然解決問題,但兩張票的座位就會不在隔壁,這是 distributed hash table 的研究範圍。

  4. 小訊息,大運算

平行化序列程式碼

lists:map 的用途是可將 list 內所有元素都放入 F 估算一次。

map(_, []) -> [];
map(F, [H|T] -> [F(H)|map(F,T)].

改呼叫新版的 pmap 就可以馬上加速序列程式

pmap 是呼叫 (catch F(H)),而 map 是呼叫 F(H),因為 pmap 必須確保當估算 F(H) 產生例外時,pmap 能正常結束。

如果 F(H) 有使用到 process dictionary,則 pmap 跟 map 的行為就不一樣了,因為 pmap 是用另一個行程來估算 F(H),所以就無法用 pmap 平行化

pmap(F, L) -> 
    S = self(),
    %% make_ref() returns a unique reference
    %%   we'll match on this later
    Ref = erlang:make_ref(), 
    Pids = map(fun(I) -> 
                       spawn(fun() -> do_f(S, Ref, F, I) end)
               end, L),
    %% gather the results
    gather(Pids, Ref).

do_f(Parent, Ref, F, I) ->    
    Parent ! {self(), Ref, (catch F(I))}.

gather([Pid|T], Ref) ->
    receive
        {Pid, Ref, Ret} -> [Ret|gather(T, Ref)]
    end;
gather([], _) ->
    [].

使用 pmap 取代 map 之前,下面這些是必須考慮的重點

  1. 共時性的單位大小 granularity
    工作量很小的時候,不要使用 pmap,因為產生 process 消耗掉的成本比直接執行 map 還多。
    例如 map(fun (I) -> 2*I, L)

  2. 不要建立太多行程
    pmap(F, L) 建立了 length(L) 個平行行程,如果 L 很大,就會一下子建立了很多行程。

  3. 思考抽象邏輯
    pmap 重視回傳值的元素順序,如果不在意回傳值的順序,可以改寫為

     pmap1(F, L) -> 
         S = self(),
         Ref = erlang:make_ref(),
         foreach(fun(I) -> 
                         spawn(fun() -> do_f1(S, Ref, F, I) end)
                 end, L),
         %% gather the results
         gather1(length(L), Ref, []).
    
     do_f1(Parent, Ref, F, I) ->                        
         Parent ! {Ref, (catch F(I))}.
    
     gather1(0, _, L) -> L;
     gather1(N, Ref, L) ->
         receive
             {Ref, Ret} -> gather1(N-1, Ref, [Ret|L])
         end.

    另外的方式,可使用固定數量 K 個行程來實現 pmap,K 可對應到 multicore 的核心數量。

小訊息,大運算

L = [L1, L2, ..., L100],
map(fun lists:sort/1, L).
其中 L的每個元素都是 1000 個亂數整數 的 list

L = [27, 27, ... , 27],
map(fun ptests:fib/1, L).
其中 L是100個數字27,計算 fibonacci(27) 一百次

分別測量兩個函數花的時間,改成 pmap,再測量一次時間。第一個排序運算,使用 pmap 時,排序本身速度快,不同 process 之間傳送的資料比較多。第二個 fibonacci 有遞迴運算,要花比較多時間計算,但傳送的資料較少。

因為 fibonacci 的資料量少,工作量大,因此可以預測在多核心環境,第二個的效率改進會比第一個大。

SMP erlang

從 Erlang R11B-0 開始,在 Intel duo/quad CPU 預設就開啟了 SMP(symmetric multiprocessing),這是指具有兩個或多個一樣的CPU,連接到單一共享記憶體的運算環境,這些CPU可能是單或多晶片。

在其他平台要開啟SMP,必須要使用 --enable-smp-support 設定。

SMP erlang 有兩個設定值,用來決定如何運作
erl -smp +S N

  1. -smp
    啟動 SMP Erlang
  2. +S N
    以 N scheduler 執行 erlang,每一個 erlagn scheduler 都是一個完整的 VM,如果不用此參數,預設數量為 SMP 機器上的邏輯處理器數量。
-module(ptests).
-export([tests/1, fib/1]).
-import(lists, [map/2]).
-import(lib_misc, [pmap/2]).

tests([N]) ->
    Nsched = list_to_integer(atom_to_list(N)),
    run_tests(1, Nsched).

run_tests(N, Nsched) ->
    case test(N) of
        stop ->
            init:stop();
        Val ->
            io:format("~p.~n",[{Nsched, Val}]),
            run_tests(N+1, Nsched)
    end.

test(1) ->
    %% Make 100 lists 
    %%   Each list contains 1000 random integers
    seed(),
    S = lists:seq(1,100),
    L = map(fun(_) -> mkList(1000) end, S),
    {Time1, S1} = timer:tc(lists,    map,  [fun lists:sort/1, L]),
    {Time2, S2} = timer:tc(lib_misc, pmap, [fun lists:sort/1, L]),
    {sort, Time1, Time2, equal(S1, S2)};
test(2) ->
    %% L = [27,27,27,..] 100 times
    L = lists:duplicate(100, 27), 
    {Time1, S1} = timer:tc(lists,    map,  [fun ptests:fib/1, L]),
    {Time2, S2} = timer:tc(lib_misc, pmap, [fun ptests:fib/1, L]),
    {fib, Time1, Time2, equal(S1, S2)};
test(3) ->
    stop.

%% Equal is used to test that map and pmap compute the same thing
equal(S,S)   -> true;
equal(S1,S2) ->  {differ, S1, S2}.

%% recursive (inefficent) fibonacci
fib(0) -> 1;
fib(1) -> 1;
fib(N) -> fib(N-1) + fib(N-2).

%% Reset the random number generator. This is so we
%% get the same sequence of random numbers each time we run
%% the program

seed() -> random:seed(44,55,66).

%% Make a list of K random numbers
%%    Each random number in the range 1..1000000
mkList(K) -> mkList(K, []).

mkList(0, L) -> L;
mkList(N, L) -> mkList(N-1, [random:uniform(1000000)|L]).

測試結果,使用 pmap 在 smp 數量超過 2 之後,速度大約都會快兩倍。

> erl -boot start_clean -noshell -smp +S 1 -s ptests tests 1 >> results
> erl -boot start_clean -noshell -smp +S 2 -s ptests tests 2 >> results
> erl -boot start_clean -noshell -smp +S 3 -s ptests tests 3 >> results

{1,{sort,37334,45348,true}}.
{1,{fib,1615806,1625368,true}}.
{2,{sort,36983,24956,true}}.
{2,{fib,1641762,812359,true}}.
{3,{sort,36484,24912,true}}.
{3,{fib,1590642,902385,true}}.

參考

Erlang and OTP in Action
Programming Erlang: Software for a Concurrent World

2014/05/12

erlang otp gen_fsm

前面提到了 otp 中,最常被提到的 gen_server gen_event supervisor,但其實還有另一個 gen_fsm behavior,fsm 就是 finite state machine 有限狀態機的縮寫。

可以在 http://erldocs.com/ 搜尋並取得 gen_fsm 的 API 文件

FSM finite state machine

finite state machine 有限狀態機,是由一組狀態、一個起始狀態、 輸入、將輸入與現在狀態轉換為下一個狀態的轉換函數所組成。

有許多東西可以用有限狀態來表達,例如紅綠燈、自動販賣機等等,以紅綠燈為例,首先,我們要知道只會有紅、黃、綠三種燈號狀態,這就是有限狀態的條件,絕對不會出現第四種狀態。

在不同狀態下,再搭配不同的輸入條件,就會讓機器從狀態1變化到狀態2。以紅綠燈來說,就是紅燈出現時,在20秒後,就自動變化到綠燈。但狀態變化也是有限制的,因為綠燈 30秒後,會變成黃燈,然後再經過 5秒變紅燈,絕對不會由綠燈直接跳到紅燈,變成黃燈,就只會再變紅燈,絕對不會突然變成綠燈。

state diagram, state transition table

要描述一個 FSM,可以使用 state diagram 或是 state transition table。State Transition Table wiki 裡面,有提到 state transition table 有一維或二維的表示方式,二維表示法還可以選擇將狀態跟改變條件寫在列或table cell 的位置。

以分析的直覺性來看,圖形的方式比較適合人眼觀看與分析,而table 的方式,看起來就比較接近撰寫程式碼的演算法。

目前\下一個 狀態 Red Green Yellow
Red X 20s X
Green X X 30s
Yellow 5s X X

code_lock 門鎖範例

fsm 官方文件 是以一個門鎖的例子,來說明如何使用 gen_gsm。

門鎖的狀態變化規則如下:

  1. 初始啟動有限狀態機時會設置鎖的密碼,然後進入 locked 狀態等待用戶按鍵輸入密碼
  2. 使用者呼叫 code_lock:button/1 輸入密碼,在輸入的過程中會記錄當前為止鍵入的資料。如果密碼錯誤或者不完整,保持 locked 狀態。
  3. 如果密碼正確,那麼就進入 open 狀態,並執行相關操作do_unlock
  4. 當 open 狀態持續一段時間後,自動執行相關操作 do_lock,1並進入 locked 狀態

啟動 fsm

gen_fsm 是由呼叫 gen_fsm:start_link({local, code_lock}, code_lock, Code, []) 開始,Code 是初始的門鎖密碼,他會 spawns and links to a new process。

handle_event 與 handle_sync_event 的差異

handle_event 是這兩個 非同步 發送 event 的事件處理函式:

send_event(FsmRef, Event) -> ok
send_all_state_event(FsmRef, Event) -> ok

handle_sync_event 是這兩個 同步 發送 event 的事件處理函式:

sync_send_event(FsmRef, Event, Timeout) -> Reply
sync_send_all_state_event(FsmRef, Event, Timeout) -> Reply

send_event 跟 send_all_state_event 的差異

send_all_state_event 就是所有狀態下該事件的處理方式都是一樣的,而send_event 則是在當前狀態下處理該事件。

拿銀行ATM機來舉例:取消操作是在任何狀態下都可以進程的,而且處理是一樣的,都是回到初始登錄界面,而提款操作則必須要在登錄驗證成功狀態下才能進行,在該狀態下的處理和其他狀態下的處理是不同的。

如果FSM在當前狀態收到的事件是無法處理的,則整個狀態機進程會被迫退出,無論FSM進程當前處於何種狀態,當gen_fsm:send_all_state_event被調用時,fsm 都會呼叫 handle_event callback函數處理。

換句話說,用 send_event 發送事件時,如果 handle_event 在目前 fsm 狀態下無法處理該事件時,此 fsm process會直接結束。

如果用 send_all_state_event,如果 handle_event 在目前 fsm 狀態下無法處理該事件時,不會造成 fsm process 直接結束

狀態中,處理事件的函式

gen_fsm的狀態是用函數表示的

Module:StateName(Event, From, StateData) -> Result

gen_fsm 的狀態處理函數名稱是此 fsm 的狀態,第一個參數 Event 是 send_event 發送的 event 內容,後面的 StateData 是 State 變數資料。

從一個狀態跳到下一個狀態是通過狀態函數的返回值控制的,NextStateName就是下一個狀態函數的名字,返回值統一這樣:

{next_state,NextStateName,NewStateData}
{next_state,NextStateName,NewStateData,Timeout}
{next_state,NextStateName,NewStateData,hibernate}
{stop,Reason,NewStateData}

程式碼

-module(code_lock).
-behaviour(gen_fsm).

-export([start_link/1]).
-export([button/1]).

-export([init/1, locked/2, open/2]).
-export([code_change/4, handle_event/3, handle_info/3, handle_sync_event/4, terminate/3]).

%% client functions
% gen_fsm 是由呼叫 code_lock:start_link(Code) 開始,Code 是初始的門鎖密碼
% 他會 spawns and links to a new process
-spec(start_link(Code::string()) -> {ok,pid()} | ignore | {error,term()}).
start_link(Code) ->
    % {local, code_lock} 代表註冊在 local 機器,名稱為 code_lock
    % 如果寫成 {global, code_lock} 就代表是呼叫了 global:register_name/2
    % 第二個參數是所有 call back function 的 module name: code_lock
    % 第三個參數是 a list of digits,會傳送到 init
    gen_fsm:start_link({local, code_lock}, code_lock, Code, []).

-spec(button(Digit::string()) -> ok).
button(Digit) ->
    gen_fsm:send_event(code_lock, {button, Digit}).

%% gen_fsm的狀態是由函數表示的
%% Module:StateName(Event, From, StateData) -> Result
%% gen_fsm 的狀態處理函數,前面的 locked, open 是此 fsm 的兩個狀態
%% 第一個參數 {button, Digit} 是 send_event 發送的 event 內容
%% 後面的 {SoFar, Code} 是 State 變數資料
%% 
%% 從一個狀態跳到下一個狀態是通過狀態函數的返回值控制的,返回值統一這樣:
%% {next_state,NextStateName,NewStateData}
%% {next_state,NextStateName,NewStateData,Timeout}
%% {next_state,NextStateName,NewStateData,hibernate}
%% {stop,Reason,NewStateData}
%% NextStateName就是下一個狀態函數的名字 
locked({button, Digit}, {SoFar, Code}) ->
    io:format("buttion: ~p, So far: ~p, Code: ~p~n", [Digit, SoFar, Code]),
    InputDigits = lists:append(SoFar, Digit),
    case InputDigits of
        Code ->
            do_unlock(),
            %% 發送事件
            {next_state, open, {[], Code}, 10000};
        Incomplete when length(Incomplete)<length(Code) ->
            {next_state, locked, {Incomplete, Code}, 5000};
        Wrong ->
            io:format("wrong passwd: ~p~n", [Wrong]),
            {next_state, locked, {[], Code}}
    end;

%% 在 locked 狀態,timeout 時,會將目前 SoFar 中記錄的輸入密碼資料清空
locked(timeout, {_SoFar, Code}) ->
    io:format("timout when waiting button inputting, clean the input, button again plz~n"),
    {next_state, locked, {[], Code}}.

%% 這個是在 open 狀態,timeout 時,會自動呼叫此事件處理函式
open(timeout, State) ->
    do_lock(),
    {next_state, locked, State}.

%%%
%% 啟動時會被呼叫的函數,回傳值裡面的 State 會在其他函數中使用
init(LockCode) ->
    io:format("init: ~p~n", [LockCode]),
    %% 初始化 fsm 時,將 fsm 狀態設定為 locked,State 內容設定為 {[], LockCode}
    {ok, locked, {[], LockCode}}.

%% 這是 send_event(FsmRef, Event) -> ok, send_all_state_event(FsmRef, Event) -> ok
%% 這兩個 非同步 發送 event 的事件處理函式
%% 當呼叫 gen_fsm:send_event 時,不需要等待 server 回應,就可繼續處理別的事情
%% 
%% send_event 跟 send_all_state_event 的差異是
%% 如果FSM在當前狀態收到的事件是無法處理的,則整個狀態機進程會被迫退出
%% 無論FSM進程當前處於何種狀態,當gen_fsm:send_all_state_event被調用時,fsm 都會呼叫 handle_event callback函數處理。
%% 換句話說,用 send_event 發送事件時,如果 handle_event 在目前 fsm 狀態下無法處理該事件時,此 fsm process會直接結束
%% 如果用 send_all_state_event,如果 handle_event 在目前 fsm 狀態下無法處理該事件時,不會造成 fsm process 直接結束
handle_event(Event, StateName, Data) ->
    io:format("handle_event... ~n"),
    unexpected(Event, StateName),
    {next_state, StateName, Data}.

%% 這是 sync_send_event(FsmRef, Event, Timeout) -> Reply, sync_send_all_state_event(FsmRef, Event, Timeout) -> Reply
%% 這兩個 同步 發送 event 的事件處理函式
%% 因為同步的關係,所以發送 event 可設定 Timeout,以避免 fsm 太久沒有回應
handle_sync_event(Event, From, StateName, Data) ->
    io:format("handle_sync_event, for process: ~p... ~n", [From]),
    unexpected(Event, StateName),
    {next_state, StateName, Data}.

%% handle_info:與gen_server類似,處理所有直接發給 FSM process 的訊息
handle_info(Info, StateName, Data) ->
    io:format("handle_info...~n"),
    unexpected(Info, StateName),
    {next_state, StateName, Data}.

terminate(normal, _StateName, _Data) ->
    io:format("terminate...~n"),
    ok.

code_change(_OldVsn, StateName, Data, _Extra) ->
    {ok, StateName, Data}.

%% Unexpected allows to log unexpected messages
unexpected(Msg, State) ->
    io:format("~p RECEIVED UNKNOWN EVENT: ~p, while FSM process in state: ~p~n",
              [self(), Msg, State]).

%% actions 
do_unlock() ->
    io:format("passwd is right, open the DOOR.~n").

do_lock() ->
    io:format("over, close the DOOR.~n").

測試

1> code_lock:start_link("test123").
init: "test123"
{ok,<0.33.0>}

%% 直接發送訊息,是由 handle_info 處理
2> pid(0,33,0)! hello.
handle_info...
<0.33.0> RECEIVED UNKNOWN EVENT: hello, while FSM process in state: locked
hello

3> code_lock:button("ab").
buttion: "ab", So far: [], Code: "test123"
ok

%% 輸入密碼逾時,SoFar 會被清空
4> timout when waiting button inputting, clean the input, button again plz
4> code_lock:button("test").
buttion: "test", So far: [], Code: "test123"
ok

%% 密碼吻合,開門了
5> code_lock:button("123").
buttion: "123", So far: "test", Code: "test123"
passwd is right, open the DOOR.
ok

%% 自動關門
6> close the DOOR.

%% 以 send_all_state_event 發送無法處理的事件 fooo
6> gen_fsm:send_all_state_event(code_lock, fooo).
handle_event...
<0.33.0> RECEIVED UNKNOWN EVENT: fooo, while FSM process in state: locked
ok

%% 以 send_event 發送無法處理的事件 fooo,會造成 fsm process 當機
7> gen_fsm:send_event(code_lock, fooo).

=ERROR REPORT==== 5-Mar-2014::16:00:49 ===
** State machine code_lock terminating
** Last event in was fooo
** When State == locked
**      Data  == {[],"test123"}
** Reason for termination =
** {function_clause,
       [{code_lock,terminate,
            [{function_clause,
                 [{code_lock,locked,
                      [fooo,{[],"test123"}],
                      [{file,
                           "d:/projectcase/erlang/erlangotp/src/code_lock.erl"},

                       {line,37}]},
                  {gen_fsm,handle_msg,7,[{file,"gen_fsm.erl"},{line,505}]},
                  {proc_lib,init_p_do_apply,3,
                      [{file,"proc_lib.erl"},{line,239}]}]},
             locked,
             {[],"test123"}],
            [{file,"d:/projectcase/erlang/erlangotp/src/code_lock.erl"},
             {line,96}]},
        {gen_fsm,terminate,7,[{file,"gen_fsm.erl"},{line,597}]},
        {proc_lib,init_p_do_apply,3,[{file,"proc_lib.erl"},{line,239}]}]}
** exception exit: function_clause
     in function  code_lock:terminate/3
        called as code_lock:terminate({function_clause,
                                       [{code_lock,locked,
                                         [fooo,{[],"test123"}],
                                         [{file,
                                           "d:/projectcase/erlang/erlangotp/src/
code_lock.erl"},
                                          {line,37}]},
                                        {gen_fsm,handle_msg,7,
                                         [{file,"gen_fsm.erl"},{line,505}]},
                                        {proc_lib,init_p_do_apply,3,
                                         [{file,"proc_lib.erl"},{line,239}]}]},
                                      locked,
                                      {[],"test123"})
     in call from gen_fsm:terminate/7 (gen_fsm.erl, line 597)
     in call from proc_lib:init_p_do_apply/3 (proc_lib.erl, line 239)

%% fsm process 確實已經被終止了
8> erlang:is_process_alive(pid(0,33,0)).
false
9>

結語

一般在討論 otp 時,只會談到 gen_server、gen_event 與 supervisor,常常會漏掉 gen_fsm,fsm 在可以根據變化的條件來決定並限制某個物件的狀態,這可以幫助我們在預期的情況下,得到物件的狀態變化,而不會因為缺少了狀態變化的判斷,直接設定物件的狀態值,導致該物件的狀態進入了意料之外的情況。

2014/05/07

Java 8 Lambda Expressions

Java 8從 2014/3/18 正式推出至今,也一個多月了,而Eclipse也在Java 8推出後宣布Eclipse 4.3.2將會支援Java8,因此花點時間測試了一下最有興趣的新特性,也就是本篇的標題,Lambda Expressions。

環境準備:

要用Eclipse來測試Java 8,需要先準備好Java 8相關的開發工具。

  1. Java Platform (JDK) 8u5,此為Java的開發工具,目前版本為8u5。

  2. eclipse 4.3.2(kepler SR2),Eclipse要4.3.2版才會支援Java 8,由於我是單純要測試而已,因此只抓標準版的Eclipse即可。

  3. Eclipse JDT升級,才能將專案的compiler設為1.8。安裝完Eclipse 4.3.2之後,再到:
    Help -> Eclipse Marketplace,搜尋Java 8,安裝Java 8 support for Eclipse Kepler SR2

  4. 開一個新的project,並將project設定為 compiler 1.8。

做完以上的準備,可以開始測試Java 8了。

Lambda語法(Syntax)介紹:

Lambda語法架構為:

Argument List Arrow Token Body
(int x, int y) -> x+y

下面有幾個範例,來對Lambda有基本的認識:

輸入沒有任何參數,輸出2:

() -> 2;  

輸入兩個int參數,輸出兩個參數相加的結果:

(int x, int y) -> x+y;

輸入字串,在console顯示輸入字串:

(String s) -> System.out.println(s);

Functional Interfaces:

對於Lambda的語法有了了解之後,接著來看看Java 8如何使用它。Java 8引進的一個新的詞彙,Functional Interfaces,指的是只擁有單一抽象方法的介面。這詞彙並不是新概念,我們之前在開發時也常常會使用到類似的介面,如比較資料時會用到的Comparator介面,或是在跑執行緒時會用到的Runnable介面。

而只要是Functional Interfaces,就可以用Lambda的方式來實作!比如說Runnable介面,原本可以透過匿名函式的方式實作,現在可改用Lambda的方式來實作,如下:

Runnable r1 = new Runnable() {
    @Override
    public void run() {
        System.out.println("run!");
    }
};

Runnable r2 = () -> System.out.println("run!"); 

再來看看Comparator介面,用匿名函式與用Lambda的實作方式:

Comparator<Integer> c1 = new Comparator<Integer>() {
        @Override
        public int compare(Integer o1, Integer o2) {
            return o1.compareTo(o2);
        }
    };

Comparator<Integer> c2 = (o1, o2) -> o1.compareTo(o2);

透過Lambda來實作可以讓程式可以精簡很多,並增加程式的可讀性。

另外,Java 8也引進了新的標注(Annotation),來標記介面為Functional Interface,只要該介面有兩個以上的自定義方法,則編譯器會報錯,此標注讓團隊的其他開發人員不會在此介面上加上別的方法。

@FunctionalInterface
interface Ti {
    public void test(String s);
}

java.util.function:

Java的型別需要事先定義好,程式才能進行編譯。因此為了搭配Lambda語法,Java 8提供新的介面讓人使用,這些介面統一放在java.util.function底下,每個介面都代表一個方法,根據輸入參數與輸出參數,定了多個介面出來。以下介紹幾個基本類型的介面:

  • Consumer< T >:輸入參數類型為T,沒有任何輸出。
  • Function< T, R >:輸入參數類型為T,輸出參數類型為R。
  • Predicate < T >:輸入參數類型為T,輸出一個布林值。
  • Supplier< T >:沒有任何輸入,輸出參數類型為T。
  • UnaryOperator< T >:輸入參數類型為T,輸出參數類型為T。

舉例來說,今天我需要一個接受字串作為輸入而沒有任何輸出的方法,則我可以使用Consumer這個介面來操作,如下:

Consumer<String> c = (s) -> System.out.println(s);
c.accept("hello world!"); // output hello world!

ArrayList與java.util.function:

接著要談到於Java 8的Collections中,新增了幾個方法可以搭配Lambda使用。這邊會說明ArrayList的三個方法:

  • forEach(Consumer<? super E> action)
  • removeIf(Predicate<? super E> filter)
  • replaceAll(UnaryOperator operator)

首先介紹forEach方法,就跟for each語法一樣,只是這邊更直覺。傳入參數為Consumer介面,Consumer介面為單一輸入參數,沒有任何輸出參數,下面為範例:

List<Integer> list = new ArrayList<>();
Collections.addAll(list, 7, 6, 2, 3);

// for each語法
for(Integer t : numbers) {
    System.out.println(t);
}

// Lambda語法
numbers.forEach((t) -> {
    System.out.println(t);
});

接著介紹removeIf方法,傳入參數為Predicate介面,此介面輸入參數類型為T,然後輸出一個布林值。也就是說,他會幫你繞行陣列的每個元素,元素會被丟到Predicate內執行,當執行結果為true,則該元素會被刪除,範例如下:

List<Integer> list = new ArrayList<>();
Collections.addAll(list, 7, 6, 2, 3);
System.out.println(list); // output:[7, 6, 2, 3]
list.removeIf((n) -> {
    if (n > 3) {
        return true;
    } else {
        return false;
    }
});
System.out.println(list); // output:[2, 3]

最後是replaceAll方法,傳入參數為UnaryOperator介面,此介面輸入參數類型和輸出參數類型是一樣的,這邊運作的方法是,回傳的參數會取代掉輸入的參數,範例如下:

List<Integer> list = new ArrayList<>();
Collections.addAll(list, 7, 6, 2, 3);
System.out.println(list); // output:[7, 6, 2, 3]
list.replaceAll((t) -> t + 1);
System.out.println(list); // output:[8, 7, 3, 4]

Method and Constructor References:

在Java 8,你可以將將方法的參考傳給變數,摘錄JSR335的內容:

Examples of method and constructor references:
System::getProperty
"abc"::length
String::length
super::toString
ArrayList::new
int[]::new

說明一下上面的例子:

  1. 如果你要需要靜態方法的參考,則可以使用 類別名稱::靜態方法名稱 來得到參考。
  2. 如果你是需要某個實例物件的方法他的參考,則可以用 實例變數::方法名稱 來得到參考。
  3. 如果你需要得到的是建構式,則可以使用 類別名稱::new 來得到參考。

下面為一個簡單的範例,透過Lambda實作一個介面,之後將介面的方法傳給變數,在用此變數呼叫該方法。

public static void main(String[] args) {
    TestInterface ti = (i) -> (i > 5) ? true : false;
    Predicate<Integer> p = ti::isGreaterThenFive;

    System.out.println(p.test(6));
}

@FunctionalInterface
interface TestInterface {
    public boolean isGreaterThenFive(Integer i);
}

參考:

Java Language Specification - 9.8 Functional Interfaces

Java SE 8: Lambda表达式

Jave SE 8: Lambda Quick Start

ArrayList(Java Platform SE 8)

Introduction to Functional Interfaces – A concept recreated in Java 8

Lambda Specification, Part C: Method and Constructor References

2014/05/05

Mac App: ForkLift特色簡介

ForkLift 除了是Finder的替代品而且也是FTP+SFTP的Client,以下就介紹這個軟體

  1. 上圖可看到檔案瀏覽的畫面預設切為左、右2塊以方便的從左右來搬移檔案,特別的是這2塊的路徑可以是指到本地端的檔案,也可以是遠端的檔案像是SFTP, FTP

  2. 2個還不夠!假設我們要將多個檔案放至遠端機器上,而這些檔案分屬在多個不同的資料夾,有時侯我們要來來回回的在這些資料夾切換,此時我們可以用Tab來將這些不同路徑的資料夾記起來(Tab的功能Finder也有提供)

  3. 管理本地的路徑與遠端的站台與路徑,有點像是我的最愛,將常用的路徑存起來,方便快速的瀏覽,按下[Favorits Manager],會列出已經存好的一些連結,可以按下[+]的按鈕來新增站台

  4. ForkLift提供了許多的便利的快捷按鈕,我們可以挑選常用的功能來顯示在畫面上,如上圖,像是Hidden Files就很方便讓我們來看隱藏檔,一般來說我們要看隱藏檔的話得到terminal裡下defaults write com.apple.finder AppleShowAllFiles TRUE指令後後再將Finder重新啟動才行,如果要將隱藏檔做隱藏的話要下defaults write com.apple.finder AppleShowAllFiles FALSE。另個常用的按鈕是[New File],這個在windows上實為一般的東西在Finder上居然沒有!不過ForkLift也提供了

  5. 其它功能還有 Multi-Rename file with sequencing, RegExp support..等

「ForkLift」的畫面很符合OSX的風格,用起來也和「Finder」簡單順手,這點也是我拋下Finder, FileZilla而使用ForkLift的原因,有興趣的可以先下載試用版來試試。

erlang otp

範例內容包含了建立伺服器、監督伺服器、登錄錯誤到日誌、偵測警報這些功能,最後再將整個程式碼包裝成一個獨立的OTP應用。

建立兩個 otp server,一個用來產生質數,一個計算面積,系統錯誤會造成當機,需要一個偵測機制來重新啟動 server,也就是 supervision tree,這可以產生一個監督者,負責觀察 server,當server crash就重新啟動。

OTP 錯誤登錄器可紀錄所有錯誤,產生錯誤報告,另外因為計算很大的質數可能會造成 CPU 過熱,為了避免這個問題,使用 OTP 事件處理框架,來產生警報,以便配置強力風扇。

  1. 說明通用事件處理器的機制
  2. 說明錯誤登錄器的機制
  3. 加入警報管理
  4. 寫出兩個應用伺服器
  5. 實作監督樹,並將伺服器加入到監督樹中
  6. 把整個程式包裝成一個 OTP 應用

通用事件處理器

當程式中,有值得注意的事情 event 發生了,就送出事件訊息到註冊行程,只需要送出訊息通知有事情發生了,不在意送出訊息後會發生什麼事情。

RegProcName ! {event, E}

第一個通用事件處理器

-module(event_handler).
-export([make/1, add_handler/2, event/2]).

%% make a new event handler called Name
%% the handler function is no_op -- so we do nothing with the event
%% 產生一個 event handler,但收到 event 後,不做任何事情
make(Name) ->
    register(Name, spawn(fun() -> my_handler(fun no_op/1) end)).

%% 發送 event及處理該 event 的函數 Fun
add_handler(Name, Fun) -> Name ! {add, Fun}.

%% generate an event
event(Name, X) -> Name ! {event, X}.

my_handler(Fun) ->
    receive
        {add, Fun1} ->
            my_handler(Fun1);
        {event, Any} ->
            % 一開始是使用 no_op(_),後來可透過 add_handler 動態修改事件處理函數
            (catch Fun(Any)),
            my_handler(Fun)
    end.

no_op(_) -> void.

測試

1> event_handler:make(errors).
true
2> event_handler:event(errors, hi).
{event,hi}

發送 event 及 處理 event 的函數

想讓 event handler 做些事情,必須寫一個 callback module,並安裝在事件處理器中。

-module(motor_controller).

-export([add_event_handler/0]).

add_event_handler() ->
    event_handler:add_handler(errors, fun controller/1).

controller(too_hot) ->
    io:format("Turn off the motor~n");
controller(X) ->
    io:format("~w ignored event: ~p~n",[?MODULE, X]).

測試

3> motor_controller:add_event_handler().
{add,#Fun<motor_controller.0.125151531>}
4> event_handler:event(errors, cool).
motor_controller ignored event: cool
{event,cool}
5> event_handler:event(errors, too_hot).
Turn off the motor
{event,too_hot}

very late binding

如果寫一個函數把 event_handler:event 封裝起來

too_hot() ->
    event_handler:event(errors, too_hot).

如果出錯就呼叫 too_hot(),這會讓事件處理的模式固定。

erlang 處理事件的方式,讓我們能解除「事件發生」和「事件處理」之間的關係,我們可以在任何時候改變處理的方式,只要發送新的事件處理函數給事件處理器,就可以動態調整事件處裡的方法。也可以利用這個機制,在不停止server的狀況下,動態升級程式。

錯誤記錄器 error logger

OTP 內建可客製化的 event logger,可從三個部份討論(1) 函數呼叫,如何登錄錯誤到日誌 (2) configuration 設定錯誤登錄資料儲存的方式 (3) 錯誤的分析報告

登錄錯誤到日誌

@spec error_logger:error_msg(String) -> ok
    發送錯誤訊息到 error logger

@spec error_logger:error_msg(Format, Data) -> ok
    發送錯誤訊息到 error logger,參數跟 io:format(Format, Data) 一樣

@spec error_logger:error_report(Report) -> ok
    發送錯誤報告到 error logger
    @type Report = [{Tag, Data} | term() | string() | term()]
    @type Tag = term()
    @type Data = term()

configuration

預設可以在 erlang shell 中看到所有錯誤,我們可以另外將錯誤寫到單一文字檔,或是製作一天一個檔案的日誌檔,也可以製作轉動的環狀日誌檔。

標準的錯誤登錄器

啟動 erl 時,加入 boot 參數,預設值就是 start_clean

這是「適合編程」的環境,只有一種簡單的錯誤日誌形式

> erl -boot start_clean

這是「適合執行產品系統」的環境,SASL: System Architecture Support Libraries 會負責錯誤登錄、負載保護等等

> erl -boot start_sasl

日誌檔案的組態設定,最好放在 configuration file 裡面。

如果啟用 SASL,但又沒有提供 config file,就會發生錯誤。

D:\projectcase\erlang\erlangotp\ebin>erl -boot start_sasl

=PROGRESS REPORT==== 20-Feb-2014::11:40:27 ===
          supervisor: {local,sasl_safe_sup}
             started: [{pid,<0.34.0>},
                       {name,alarm_handler},
                       {mfargs,{alarm_handler,start_link,[]}},
                       {restart_type,permanent},
                       {shutdown,2000},
                       {child_type,worker}]
.....

=PROGRESS REPORT==== 20-Feb-2014::11:40:27 ===
         application: sasl
          started_at: nonode@nohost
Eshell V5.10.4  (abort with ^G)
1>
控制日誌的內容

錯誤登錄會自動產生三種報告

  1. 監督者報告
    當 OTP 監督者開始或停止監督一個 process 時 所發出的
  2. 進度報告
    當 OTP 監督者啟動或停止時 所發出的
  3. 當機報告
    當一個 OTP 行為終止的理由不正常 或 關閉時 所發出的

另外可以主動呼叫 error_handler module 的 function,來產生幾種等級的日誌報告,error_type 的類型有 error | error_report | info_msg | info_report | warning_msg | warning_report | crash_report | supervisor_report | progress,在分析日誌時,就能使用標籤來幫助我們決定那個日誌項目需要被檢視。

範例1

這會得到錯誤報告,而不是進度或其他報告

%% elog1.config
%% no tty 
[{sasl, [
     {sasl_error_logger, false}
    ]}].

測試

>erl -boot start_sasl -config elog1
Eshell V5.10.4  (abort with ^G)
1> error_logger:error_msg("I'm Error\n").
=ERROR REPORT==== 20-Feb-2014::11:54:08 ===
I'm Error
ok
範例2

會把 shell 所有東西複製到檔案中

%% elog2.config
%% single text file - minimal tty
[{sasl, [
     %% All reports go to this file
     {sasl_error_logger, {file, "d:/temp/error_logs/logfile"}}
    ]}].

測試

>erl -boot start_sasl -config elog2
Eshell V5.10.4  (abort with ^G)
1> error_logger:error_msg("I'm Error\n").

=ERROR REPORT==== 20-Feb-2014::12:01:01 ===
I'm Error
ok

如果 d:/temp/error_logs 目錄不存在,就不會產生檔案 logfile,檔案的內容如下:

=PROGRESS REPORT==== 20-Feb-2014::12:00:59 ===
          supervisor: {local,sasl_safe_sup}
             started: [{pid,<0.35.0>},
                       {name,alarm_handler},
                       {mfargs,{alarm_handler,start_link,[]}},
                       {restart_type,permanent},
                       {shutdown,2000},
                       {child_type,worker}]
....
範例3

所有錯誤日誌都會進入 rotating log 裡面

%% rotating log and minimal tty
[{sasl, [
     {sasl_error_logger, false},    
     %% define the parameters of the rotating log
     %% the log file directory 這裡是填目錄
     {error_logger_mf_dir,"d:/temp/error_logs"},    
         %% # bytes per logfile
     {error_logger_mf_maxbytes,10485760}, % 10 MB
         %% maximum number of logfiles
     {error_logger_mf_maxfiles, 10}
    ]}].
> erl -boot start_sasl -config elog3
範例4

在正式環境,增加 {errlog_type, error} 只記錄 error

%% rotating log and errors
[{sasl, [     
     %% minimise shell error logging
     {sasl_error_logger, false},
         %% only report errors
     {errlog_type, error},
     %% define the parameters of the rotating log
     %% the log file directory
     {error_logger_mf_dir,"d:/temp/error_logs"},    
         %% # bytes per logfile
     {error_logger_mf_maxbytes,10485760}, % 10 MB
         %% maximum number of
     {error_logger_mf_maxfiles, 10}
    ]}].

分析錯誤

rb 模組負責分析錯誤。

rb:help(). 可取得 help
rb:start([{max, 20}]). 啟動報告瀏覽器,只讀取 20 筆記錄
rb:show(1). 顯示 1 號錯誤
rb:list(). 錯誤列表
rb:grep(RegExp). 找出所有符合 RegExp 正規表示式 的報告

>erl -boot start_sasl -config elog3
1> rb:start([{max, 20}]).
rb: reading report...done.
{ok,<0.42.0>}
2> rb:show(1).

PROGRESS REPORT  <0.7.0>                                    2014-02-20 15:57:19
===============================================================================
application                                                                sasl
started_at                                                        nonode@nohost

ok

警報管理

警報處理器是 OTP gen_event 行為的 callback module。

-module(my_alarm_handler).
-behaviour(gen_event).

%% gen_event callbacks
-export([init/1, handle_event/2, handle_call/2, 
         handle_info/2, terminate/2]).

%% init(Args) must return {ok, State}
init(Args) ->
    io:format("*** my_alarm_handler init:~p~n",[Args]),
    {ok, 0}.

handle_event({set_alarm, tooHot}, N) ->
    error_logger:error_msg("*** Tell the Engineer to turn on the fan~n"),
    {ok, N+1};
handle_event({clear_alarm, tooHot}, N) ->
    error_logger:error_msg("*** Danger over. Turn off the fan~n"),
    {ok, N};
handle_event(Event, N) ->
    io:format("*** unmatched event:~p~n",[Event]),
    {ok, N}.

handle_call(_Request, N) -> Reply = N, {ok, Reply,  N}.

handle_info(_Info, N)    -> {ok, N}.

terminate(_Reason, _N)   -> ok.

測試

>erl -boot start_sasl -config elog3

% 一開始的 set_alarm,什麼事都不會發生,因為預設的處理器不做任何事情
1> alarm_handler:set_alarm(tooHot).
ok

=INFO REPORT==== 20-Feb-2014::15:01:57 ===
    alarm_handler: {set,tooHot}

% 替換 alarm_handler
2> gen_event:swap_handler(alarm_handler, {alarm_handler, swap}, {my_alarm_handler, xyz}).
*** my_alarm_handler init:{xyz,{alarm_handler,[tooHot]}}
ok

% 再一次呼叫 set_alarm 就會改用自訂的 alarm handler
3> alarm_handler:set_alarm(tooHot).

=ERROR REPORT==== 20-Feb-2014::15:02:49 ===
*** Tell the Engineer to turn on the fan
ok

% 清除 tooHot 的警報
4> alarm_handler:clear_alarm(tooHot).

=ERROR REPORT==== 20-Feb-2014::15:03:09 ===
*** Danger over. Turn off the fan
ok

查看警報的日誌報告

>erl -boot start_sasl -config elog3
Eshell V5.10.4  (abort with ^G)
1> rb:start([{max,20}]).
rb: reading report...done.
{ok,<0.42.0>}
2> rb:list().
  No                Type   Process       Date     Time
  ==                ====   =======       ====     ====
  14            progress  <0.30.0> 2014-02-20 15:57:19
  13            progress  <0.30.0> 2014-02-20 15:57:19
  12            progress  <0.30.0> 2014-02-20 15:57:19
  11            progress  <0.30.0> 2014-02-20 15:57:19
  10            progress  <0.24.0> 2014-02-20 15:57:19
   9            progress  <0.30.0> 2014-02-20 15:58:12
   8         info_report  <0.30.0> 2014-02-20 16:00:25
   7               error  <0.30.0> 2014-02-20 16:00:36
   6               error  <0.30.0> 2014-02-20 16:00:39
   5            progress  <0.30.0> 2014-02-20 16:02:03
   4            progress  <0.30.0> 2014-02-20 16:02:03
   3            progress  <0.30.0> 2014-02-20 16:02:03
   2            progress  <0.30.0> 2014-02-20 16:02:03
   1            progress  <0.24.0> 2014-02-20 16:02:03
ok
3> rb:show(6).

ERROR REPORT  <0.34.0>                                      2014-02-20 16:00:39
===============================================================================

*** Danger over. Turn off the fan
ok
4> rb:show(7).

ERROR REPORT  <0.34.0>                                      2014-02-20 16:00:36
===============================================================================

*** Tell the Engineer to turn on the fan
ok
4> rb:stop().
ok

應用伺服器

有兩個伺服器

質數

-module(prime_server).
-behaviour(gen_server).

-export([new_prime/1, start_link/0]).

%% gen_server callbacks
-export([init/1, handle_call/3, handle_cast/2, handle_info/2,
     terminate/2, code_change/3]).

%% client functions
start_link() ->
    %% 呼叫 gen_server:start_link(Name, CallBackMod, StartArgs, Opts) 啟動 server
    %% 第一個呼叫的是 Mod:init(StartArgs)
    gen_server:start_link({local, ?MODULE}, ?MODULE, [], []).

new_prime(N) ->
    %% 20000 is a timeout (ms)
    gen_server:call(?MODULE, {prime, N}, 20000).


%% 6 server callback functions

%% 啟動時會被呼叫的函數,回傳值 {ok, State} 裡面的 State 會在其他函數中使用
init([]) ->
    %% Note we must set trap_exit = true if we 
    %% want terminate/2 to be called when the application is stopped
    %% 一定要設定 trap_exit 為 true,這樣在  app 停止時,才會呼叫 terminate/2
    process_flag(trap_exit, true),
    io:format("~p starting~n",[?MODULE]),
    {ok, 0}.

handle_call({prime, K}, _From, N) -> 
    {reply, make_new_prime(K), N+1}.

handle_cast(_Msg, N)  -> {noreply, N}.

handle_info(_Info, N)  -> {noreply, N}.

terminate(_Reason, _N) -> 
    io:format("~p stopping~n",[?MODULE]),
    ok.

code_change(_OldVsn, N, _Extra) -> {ok, N}.

% private function
make_new_prime(K) ->
    if
    K > 100 ->
        alarm_handler:set_alarm(tooHot),
        N = lib_primes:make_prime(K),
        alarm_handler:clear_alarm(tooHot),
        N;
    true ->
        lib_primes:make_prime(K)
    end.

面積

-module(area_server).
-behaviour(gen_server).

-export([area/1, start_link/0]).

%% gen_server callbacks
-export([init/1, handle_call/3, handle_cast/2, handle_info/2,
     terminate/2, code_change/3]).

%% client functions
start_link() ->
    gen_server:start_link({local, ?MODULE}, ?MODULE, [], []).

area(Thing) ->
    gen_server:call(?MODULE, {area, Thing}).


%% 6 server callback functions
%% 啟動時會被呼叫的函數,回傳值 {ok, State} 裡面的 State 會在其他函數中使用
init([]) ->
    %% Note we must set trap_exit = true if we 
    %% want terminate/2 to be called when the application
    %% is stopped
    %% 一定要設定 trap_exit 為 true,這樣在  app 停止時,才會呼叫 terminate/2
    process_flag(trap_exit, true),
    io:format("~p starting~n",[?MODULE]),
    {ok, 0}.

handle_call({area, Thing}, _From, N) -> {reply, compute_area(Thing), N+1}.

handle_cast(_Msg, N)  -> {noreply, N}.

handle_info(_Info, N)  -> {noreply, N}.

terminate(_Reason, _N) -> 
    io:format("~p stopping~n",[?MODULE]),
    ok.

code_change(_OldVsn, N, _Extra) -> {ok, N}.

%% private function
compute_area({square, X})       -> X*X;
compute_area({rectongle, X, Y}) -> X*Y.

監督樹

所有監督樹都是行程樹,樹上方的行程(監督者)會監控下面的行程(工作者),如果工作者發生問題,監督者就可以重新啟動它。監督樹有兩種:

  1. 一對一監督樹
    如果工作者失敗,就會被監督者重新啟動。監督者會同時監督多個行程,一對一就是針對每一個行程,都會單獨地進行重新啟動的機制。
  2. 一對多監督樹
    如果任一工作者失敗,監督者監控的所有工作者都會被 kill 掉,然後重新啟動所有工作行程。

監督樹的指定是透過下面這樣的函數

init(...) ->
    {ok, {RestartStrategy, MaxRestarts, Time},
        [Worker1, Worker2, ...]}.

RestartStrategy 有兩種 (1) one_for_one (2) all_for_one
MaxRestarts 與 Time 是設定一個 「重新啟動的頻率」,如果監督者在 Time 秒內重新啟動工作者超過 MaxRestarts 次,此監督者就會終止所有工作行程,並終止自己。這個機制是為了避免發生「行程當機->重新啟動->行程當機」 的無窮迴圈。

Worker1, Worker2,... 都是 tuple,描述如何重新啟動每一個工作者行程。

Worker
    {Tag, {Mod, Fun, ArgList},
        Restart,
        Shutdown,
        Type,
        [Mod1]}

Tag
    atom,用來稱呼該工作者行程
{Mod, Fun, ArgList}
    定義了監督者會用到的函數,當作 apply(Mod, Fun, ArgList) 的參數
Restart = permanent | transient | temporary
    permanent 行程一定會被重新啟動
    transient 只有在「因非正常結束碼而停止」之後,才會重新啟動
    temporary 不會被重新啟動
Shutdown
    停止 server 時所能耗用的最大時間,超過時間,就會被直接 kill
Type = worker | supervisor
    被監督行程的型別,我們可以在工作者行程的位置改放監督者行程,建構出監督者的樹
[Mod1]
    如果子行程是監督者或 gen_server 行為callback module,這就是 callback module 的名稱

範例

-module(sellaprime_supervisor).
-behaviour(supervisor).

-export([start/0, start_in_shell_for_testing/0, start_link/1, init/1]).

start() ->
    spawn(fun() ->
                  supervisor:start_link({local,?MODULE}, ?MODULE, _Arg = [])
          end).

start_in_shell_for_testing() ->
    {ok, Pid} = supervisor:start_link({local,?MODULE}, ?MODULE, _Arg = []),
    unlink(Pid).

start_link(Args) ->
    supervisor:start_link({local,?MODULE}, ?MODULE, Args).

init([]) ->
    %% Install my personal error handler
    %  替換自訂的 error handler
    gen_event:swap_handler(alarm_handler, 
                           {alarm_handler, swap},
                           {my_alarm_handler, xyz}),

    {ok, {{one_for_one, 3, 10},
          [{tag1, 
            {area_server, start_link, []},
            permanent, 
            10000, 
            worker, 
            [area_server]},
           {tag2, 
            {prime_server, start_link, []},
            permanent, 
            10000, 
            worker, 
            [prime_server]}
          ]}}.

啟動系統

>erl -boot start_sasl -config elog3
Eshell V5.10.4  (abort with ^G)
1> sellaprime_supervisor:start_in_shell_for_testing().
*** my_alarm_handler init:{xyz,{alarm_handler,[]}}
area_server starting
prime_server starting
true
2> area_server:area({square, 10}).
100

% area_server 會停掉,並重新啟動
3> area_server:area({rectangle, 10, 20}).
area_server stopping
area_server starting
** exception exit: {{function_clause,
                        [{area_server,compute_area,
                             [{rectangle,10,20}],
                             [{file,
                                  "d:/projectcase/erlang/erlangotp/src/area_serv
er.erl"},
                              {line,50}]},
                         {area_server,handle_call,3,
                             [{file,
                                  "d:/projectcase/erlang/erlangotp/src/area_serv
er.erl"},
                              {line,37}]},
                         {gen_server,handle_msg,5,
                             [{file,"gen_server.erl"},{line,585}]},
                         {proc_lib,init_p_do_apply,3,
                             [{file,"proc_lib.erl"},{line,239}]}]},
                    {gen_server,call,[area_server,{area,{rectangle,10,20}}]}}
     in function  gen_server:call/2 (gen_server.erl, line 180)
4>
=ERROR REPORT==== 20-Feb-2014::18:08:09 ===
** Generic server area_server terminating
** Last message in was {area,{rectangle,10,20}}
** When Server state == 1
** Reason for termination ==
** {function_clause,
       [{area_server,compute_area,
            [{rectangle,10,20}],
            [{file,"d:/projectcase/erlang/erlangotp/src/area_server.erl"},
             {line,50}]},
        {area_server,handle_call,3,
            [{file,"d:/projectcase/erlang/erlangotp/src/area_server.erl"},
             {line,37}]},
        {gen_server,handle_msg,5,[{file,"gen_server.erl"},{line,585}]},
        {proc_lib,init_p_do_apply,3,[{file,"proc_lib.erl"},{line,239}]}]}
4> area_server:area({square, 20}).
400
5> prime_server:new_prime(20).
Generating a 20 digit prime ..........................
11511342604390163281
6> prime_server:new_prime(120).
Generating a 120 digit prime .
=ERROR REPORT==== 20-Feb-2014::18:08:48 ===
*** Tell the Engineer to turn on the fan
................................................................................
31109310729316846838871494679201035944025034010885289775272667568919068598600821
8311374378220338982779418876998717503511

=ERROR REPORT==== 20-Feb-2014::18:08:51 ===
*** Danger over. Turn off the fan

接下來查閱報表。

7> rb:start([{max, 20}]).
rb: reading report...done.
{ok,<0.53.0>}
8> rb:list().
  No                Type      Process       Date     Time
  ==                ====      =======       ====     ====
  13            progress     <0.30.0> 2014-02-20 18:07:21
  12            progress     <0.30.0> 2014-02-20 18:07:21
  11            progress     <0.30.0> 2014-02-20 18:07:21
  10            progress     <0.30.0> 2014-02-20 18:07:21
   9            progress     <0.24.0> 2014-02-20 18:07:21
   8            progress     <0.24.0> 2014-02-20 18:07:39
   7            progress     <0.24.0> 2014-02-20 18:07:39
   6               error     <0.24.0> 2014-02-20 18:08:09
   5        crash_report  area_server 2014-02-20 18:08:09
   4   supervisor_report     <0.24.0> 2014-02-20 18:08:09
   3            progress     <0.24.0> 2014-02-20 18:08:09
   2               error     <0.30.0> 2014-02-20 18:08:48
   1               error     <0.30.0> 2014-02-20 18:08:51
ok
9> rb:show(5).

CRASH REPORT  <0.43.0>                                      2014-02-20 18:08:09
===============================================================================
Crashing process
   initial_call                              {area_server,init,['Argument__1']}
   pid                                                                 <0.43.0>
   registered_name                                                  area_server
   error_info
         {exit,
            {function_clause,
                [{area_server,compute_area,
                     [{rectangle,10,20}],
                     [{file,
                          "d:/projectcase/erlang/erlangotp/src/area_server.erl",
                      {line,50}]},
                 {area_server,handle_call,3,
                     [{file,
                          "d:/projectcase/erlang/erlangotp/src/area_server.erl",
                      {line,37}]},
                 {gen_server,handle_msg,5,
                     [{file,"gen_server.erl"},{line,585}]},
                 {proc_lib,init_p_do_apply,3,
                     [{file,"proc_lib.erl"},{line,239}]}]},
            [{gen_server,terminate,6,[{file,"gen_server.erl"},{line,744}]},
             {proc_lib,init_p_do_apply,3,
                 [{file,"proc_lib.erl"},{line,239}]}]}
   ancestors                                   [sellaprime_supervisor,<0.40.0>]
   messages                                                                  []
   links                                                             [<0.42.0>]
   dictionary                                                                []
   trap_exit                                                               true
   status                                                               running
   heap_size                                                                987
   stack_size                                                                27
   reductions                                                               200

ok

包裝應用

%% sellaprime.app
{application, sellaprime, 
 [{description, "The Prime Number Shop"},
  {vsn, "1.0"},
  {modules, [sellaprime_app, sellaprime_supervisor, area_server, 
         prime_server, lib_lin, lib_primes, my_alarm_handler]},    
  {registered,[area_server, prime_server, sellaprime_super]},
  {applications, [kernel,stdlib]},
  {mod, {sellaprime_app,[]}},
  {start_phases, []}
 ]}.

sellaprime.app 的 callback module,必須有 start/2 跟 stop/1 function

% sellaprime_app.erl
-module(sellaprime_app).
-behaviour(application).
-export([start/2, stop/1]).

start(_Type, StartArgs) ->
    sellaprime_supervisor:start_link(StartArgs).

stop(_State) ->
    ok.

測試
application:loaded_applications(). 取得 otp 載入的applications
application:load(sellaprime). 載入 sellaprime.app
application:start(sellaprime). 啟動 sellaprime
application:stop(sellaprime). 停止 sellaprime
application:unload(sellaprime). 卸載 sellaprime.app

>erl -boot start_sasl -config elog3
Eshell V5.10.4  (abort with ^G)
1> application:loaded_applications().
[{kernel,"ERTS  CXC 138 10","2.16.4"},
 {sasl,"SASL  CXC 138 11","2.3.4"},
 {stdlib,"ERTS  CXC 138 10","1.19.4"}]
2> application:load(sellaprime).
ok
3> application:loaded_applications().
[{sellaprime,"The Prime Number Shop","1.0"},
 {kernel,"ERTS  CXC 138 10","2.16.4"},
 {sasl,"SASL  CXC 138 11","2.3.4"},
 {stdlib,"ERTS  CXC 138 10","1.19.4"}]
4> application:start(sellaprime).
*** my_alarm_handler init:{xyz,{alarm_handler,[]}}
area_server starting
prime_server starting
ok
5> area_server:area({square, 20}).
400
6> prime_server:new_prime(20).
Generating a 20 digit prime ...................
28361723754284313301
7> application:stop(sellaprime).
prime_server stopping
area_server stopping
ok
=INFO REPORT==== 21-Feb-2014::10:29:12 ===
    application: sellaprime
    exited: stopped
    type: temporary
8> application:loaded_applications().
[{sellaprime,"The Prime Number Shop","1.0"},
 {kernel,"ERTS  CXC 138 10","2.16.4"},
 {sasl,"SASL  CXC 138 11","2.3.4"},
 {stdlib,"ERTS  CXC 138 10","1.19.4"}]
9> application:unload(sellaprime).
ok
10> application:loaded_applications().
[{kernel,"ERTS  CXC 138 10","2.16.4"},
 {sasl,"SASL  CXC 138 11","2.3.4"},
 {stdlib,"ERTS  CXC 138 10","1.19.4"}]
11> init:stop().
ok

完整的應用包含的檔案

  1. area_server.erl
    area_server 這是 gen_server 的 callback module
  2. prime_server.erl
    prime_server 這是 gen_server 的 callback module
    2.1 lib_primes.erl, lib_lin.erl
    產生 primes 的 module
  3. sellaprime_supervisor.erl
    監督者 callback module
  4. sellaprime_app.erl
    sellaprime application callback module
  5. my_alarm_handler.erl
    gen_event 事件處理 callback module
  6. sellaprime.app
    sellaprime application
  7. elog3.config
    error logger configuration

運作流程如下

  1. 啟動系統 > erl -boot start_sasl -config elog3.config
    sellaprime.app 必須在 erlang 啟動的根目錄,或在該目錄的某個次目錄
    應用控制器會在 sellaprime.app 中取得 {mod, ...} 宣告,也就是 sellaprime_app.erl

  2. 呼叫 sellaprime_app:start/2

  3. sellaprime_app:start/2 內部呼叫 sellaprime_supervisor:start_link/2,然後啟動了 sellaprime 監督者

  4. 呼叫 sellaprime_supervisor:init/1,這會安裝一個 error logger 處理器,並回傳重新啟動的策略

  5. sellaprime 的監督者會啟動 prime_server 與 area_server

  6. 呼叫 application:stop(sellaprime) 或 init:stop() 就可以停止 sellaprime application

GUI 應用監控器

這是可以檢視 application 的 GUI 程式

appmon:start().

參考

Erlang and OTP in Action
Programming Erlang: Software for a Concurrent World

2014/05/02

再談Objective-C中的Block - 使用Block的注意事項

前言

簡單回顧:Block是C語言的Closure語法,Block中可以有條件地使用Block以外所定義的變數(詳情請參閱前文)。
而此篇要稍微深入探討使用Block時應注意的事項。

Block所佔用的記憶體區域被配置在堆疊上

考慮以下程式碼:
void (^block)();
if {
    block = ^{ //do something 1...  };
}
else {
    block = ^{ //do something 2...  };
}
block();
我們可能很容易因邏輯需求而寫出這樣的程式碼。
然而,由於Block所佔用的記憶體區域被配置在堆疊上;因此,Block所占用的記憶體空間只在該作用域內有效。 在C語言中,即便是在同一個函式中,一個大括號包起來的就是一個獨立的作用域[1];這表示,過了該作用域之後,Block所占用的記憶體空間可能不再被保證有效。
程式可能出錯,也可能不會;端看該Block所占用的記憶體空間是否被覆蓋掉 。
不過,一個簡單的做法就可以解決此問題,使用copy[2]:
void (^block)();
if {
    block = [^{ //do something 1...  } copy];
}
else {
    block = [^{ //do something 2...  } copy];
}
block();
如果指向該Block的變數是物件的property中,也應該使用copy:
@property (copy) void (^myblock)(void);
以copy建立副本,確保存放該Block的變數,其指向的block是有效的。

避免Block隱式保留self而造成的保留循環

我曾在前文提及,
Block會隱式地保留self:
@implementation ViewController {
    int myValue;
}

- (void)viewDidLoad
{
    myValue = 123;
    void(^myBlock)(void) = ^{
        //其實是存取self->myValue, block隱式保留self
        NSLog(@"myValue in block:%i",myValue); 

    };      
}
由於Block也是Objective-C物件,也會被參考計數;因此,如下寫法:
@interface ViewController : UIViewController
@property (copy) void (^myblock)(void);
@end    

@implementation ViewController {
    int myValue;
}

- (void)viewDidLoad
{
    myValue = 123;
    self.myblock = ^{
        //存取self->myValue, block隱式保留self
        NSLog(@"myValue in block:%i",myValue); 

    };      
}
會使myblock保留self,而self本身又擁有myblock,兩個物件互相參考, 造成所謂的保留循環,使兩者的記憶體永遠不會被釋放。

解法1:使用weak參考的self

@interface ViewController : UIViewController
@property (copy) void (^myblock)(void);
@end    

@implementation ViewController {
    int myValue;
}

- (void)viewDidLoad
{
    myValue = 123;
    ViewController* __weak weakSelf = self;
    self.myblock = ^{
        //確認self尚未被取消配置
        if(weakSelf != nil) {
            NSLog(@"myValue in block:%i",weakSelf->myValue); 
        }                       
    };      
}
使用另一個變數指向self,並使用__weak修飾子, 如此一來,該變數(weakSelf)不會保留住self(不會增加參考計數)。
而Block使用的是weakSelf而非self本身,因此不會保留self。
唯一要注意的是,在self被取消配置時,weakSelf會被設為nil; 因此在執行前,需要檢查一下weakSelf是否為nil(雖然在正確的邏輯下不應該發生); 做好例外處理。

解法2:在block執行完後,將指向block的property設為nil

@interface ViewController : UIViewController
@property (copy) void (^myblock)(void);
@end    

@implementation ViewController {
    int myValue;
}

- (void)viewDidLoad
{
    myValue = 123;
    self.myblock = ^{
        //存取self->myValue, block隱式保留self
        NSLog(@"myValue in block:%i",myValue); 


        self->myblock = nil;            
    };      
}
更簡單的方法是在執行完block的時候,將myblock設為nil,解除互相參考的狀態。 然而,倘若block永遠都沒有執行,則此保留循環的狀況也將不會解除。 因此,務必確認block一定會被執行;否則,建議使用解法1。

結語

Block的使用一直都是把雙面刃,如果你不夠了解Block的運作機制以及其行為模式,當然很有可能造成非預期的錯誤。 Block的語法乍看之下複雜,不過其實並不難。如果你先前曾有一些Script語言的經驗,諸如: JavaScript的函式實字,或是Python的lambda表示式...等等,則應該不難理解Block的概念。
在iOS程式開發中,許多處理多執行緒的API,也漸漸地使用Block,如GCD, NSBlockOperation...等等,因此,理解並活用Block也逐漸成為新一代iOS程式設計師應當做好的基本功。

Reference