2014/5/26

Rubik's Cube : 8355 解法筆記

一般學習解三階Rubik's Cube,都會用層解法,也就是 LBL,高階一點的,更高速的解法,會用 CFOP,不過已經背不了太多公式的我,只能學習用 8355 解法。

8355 詳細的影片及介紹,可以參閱這篇文章:[方塊]8355法,不用背公式也能解方塊!你相信嗎?! ,仔細慢慢看影片,就可以把這個方式學起來。

以下是我自己的筆記,簡單寫起來,過一陣子看看自己能不能看得懂。這個方法,除了過程中要記幾個口訣之外,大多數的時候,是用觀察的方式,決定要怎麼轉方塊。

三階方塊可分「中心」「邊塊」「角塊」三種:

「中心」塊有6個,相對位置一定不會改變。
「邊塊」有12個,只有兩個顏色。
「角塊」有8個,有三個顏色。

第一步驟:8

目標:以白色為底,完成底層十字以及八個白色塊,紅綠白那一個不用做。

方法:

先完成十字

  1. 找到中心白色,放在正下方。

  2. 移動4個白色邊塊,最後朝上。

  3. 找到側面顏色對齊,例如紅色,然後用右手往下轉 180度。

  4. 再找其他側面顏色對齊,重複上一步。

方塊翻過來,白色朝上,完成其他白色角塊,紅綠白那一個不用做,那一行留作工作區

  1. 在下面找到白色角塊,找到應該放的位置,避開後面兩階,試著用右手往上轉。

  2. 成功後,把白色方塊復原

  3. 重複上兩個步驟

第二步驟:3

目標:完成第二層三個邊塊,但工作區紅綠邊塊不做

口訣:開門 - 上車 - 關門

方法:可用右或左手,往上轉一次,上層往左(或往右),再往下一次復原。

第三步驟:5

目標:完成剩下 5 個邊塊,4 個黃跟其他顏色,1 個紅綠

口訣:

  1. 位置互換:順上順下、順上順下
  2. 兩邊翻面:(左手)上順下逆、(右手)上逆下順

第四步驟:5

目標:完成剩下5個角塊

口訣:

+1 : 下左上右
-1 : 左下右上

+1 或-1 都會造成工作區的兩個角塊位置調換
+2 或-2 又會換回來,但方向會轉動,+2會逆時針旋轉,-2會順時針旋轉
+3 或 -3時,上下角塊會對調位置,但底會朝上,頂會朝下

當累計到 +6 或 -6 時,視作歸零,歸零時,下層的8顆一定是正確的。

做到剩最後兩顆時,建議先將底層歸零。

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/5/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/5/14

審判者傳奇:鋼鐵心 - 布蘭登.山德森

只要是異於常人,都是異能者,有很多故事都是用異能者來當主角,這些異能者有好人也有壞人,最明顯的例子就是 X 戰警,正反兩邊的異能者互相對抗。審判者傳奇:鋼鐵心這個故事不一樣的地方在於,主角大衛是個普通人。

大衛因為小時候在銀行金庫外面,遇到的一場災難,他的父親竟然能讓有史以來最強悍的鋼鐵心流血,後來大衛立志,要殺死鋼鐵心,用了一切科學的分析方法,收集資料,分析每個異能者的能力與弱點,最後成為審判者團隊的成員之一。

這個故事從頭到尾都是用大衛第一人稱的敘述方式寫完的,所以並沒有對其他人物有很多著墨,所有對其他人的描繪,都是從大衛觀察的角度撰寫的,所以即使是整個故事的大反派鋼鐵心,他所佔的篇幅也不多。

攻殼機動隊 Ghost in the Shell 裡面的公安九課,也用了類似的方式,在幾位義體強化的成員中,甚至是全身義體的少校草薙素子帶領下,還是選了一位普通人德古沙作為團隊的成員,這個成員還徹底復古到是使用左輪手槍,事實上有些故事,也因為德古沙不受電腦病毒影響,而能處理一些其他成員辦不到的事情。

大衛沒有天生的異能,但他發展出一套研究異能者的方法,也能快速分析出異能者的弱點,這也是因為他沒有仰仗的異能,才開發出來的一種分析能力。

關於梅根這位異能者熾燄,能力的設定有點奇怪,使用能力會脾氣暴躁,死亡後一天內會復活,但復活後心智會改變,還可以用錄影的影片回憶,這樣的異能者,卻被鋼鐵心派來當作臥底,未免也太不安全了。

鋼鐵心只能被不怕他的人殺死,當鋼鐵心開槍時,他認為他要殺死的是大衛,可是卻引爆了他所不知道的炸彈,這卻在無意中,殺死了他自己,這對一個普通人大衛來說,已經是最好的戰鬥方法了,但我們也知道,這種方法只能是一次意外,如果下一本審判者傳奇,大衛還能用這個方法傷人,那我只能說,作者寫作的功力真的太強悍了,可以讓大衛成為「合理的意外」這樣的另類異能者。

選擇普通人當主角,讓故事變得特別,但同一個方法沒辦法用太多次,不過作者應該已經有想到這個問題,所以他設計了能傳遞異能給普通人的異能者,也許大衛能用這樣的方法,在續集裡面,借用一些異能來使用,但這樣處理的話,好像又變成了 BEN 10 的小班。

2014/5/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/5/5

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/5/3

WWW.甦醒 - 羅伯特.索耶

當網路有了自己的意識,這個新的智慧生命體,並不知道自己是怎麼誕生的,但唯一知道的是,他就像是新生兒一樣,渴望用自己所有的方式,學習並跟外界接觸,WWW.甦醒的故事內容,就是在講這樣的生命體,怎麼學習與完備自己的獨立意識。

主角凱特琳雖然天生就有個數學頭腦,但是她只能透過電子書,並在電腦前面學習,因為她是個盲人,來自日本的黑田博士因為自己的研究計畫,發展出一個可直接修正視網膜解碼資料,並傳送給視神經的機器eyepod,植入到凱特琳的左眼,幫助凱特琳重見光明。

然而eyepod使用時,必須接入網路,一方面要更新程式接受訊號,另一方面要把eyepod處理的結果傳送到黑田博士的電腦中,凱特琳慢慢地發現,這個網路意識體在學習與成長。

不知道大家有沒有在自己的腦袋裡面自言自語的經驗,就是沒有用嘴巴講話,但是在自己的腦袋裡面,跟自己對話,有時候可能是腦袋裡面有問個問題,然後隔了一陣子,又在腦袋裡面自己回答了。

有時候是聽或看到一個問題,或是由感官接收的一些資訊,腦袋裡面就開始自己講話了,可能是回答或是思考的過程,或是在回想在其他地方有沒有看到類似的東西,很像是在做數學解題時,實做的解答過程,有時候會想錯,擦掉了然後再繼續解題。

我覺得 WWW.甦醒 就是在說明這種經驗,就像是自己的意識裡面,有著另一個獨立的意識個體,會跟主體自然地互動與對話。當這種意識太過清晰,浮現到主體的表面時,應該就像是幻覺或是幻聽一樣,已經無法分辨什麼是真正存在於現實世界當中了。

WWW.甦醒 是把幻覺轉化為網路的外在獨立意識,最後甚至讓這個獨立意識透過 email 跟主角凱特琳互動,也確認這個意識生命體是真實存在的,有自由思考的能力。

網路意識在故事的前一大半,都只能自言自語的時候,有時候讀起來會覺得搞不懂這些章節,到底在寫什麼,也真多虧作者能用文字,把網路意識甦醒的過程寫出來,讀者在閱讀時,也必須想像自己孑然一身,從一個空無的個體,慢慢地進行思考,然後自言自語講出來的話,才比較能接受這些部份的文字內容。