Actor模型
Actor模型為並行而生,具Wikipedia中的描述,它原本是為大量獨立的微型處理器所構建的高性能網絡而設計的模型。而目前,單台機器也有了多個獨立的計算單元,這就是為什么在並行程序愈演愈烈的今天,Actor模型又重新回到了人們的視線之中了。Actor模型的理念非常簡單:天下萬物皆為Actor,Actor之間通過發送消息進行通信。Actor模型的執行方式有兩個特點:
- 每個Actor,單線程地依次執行發送給它的消息。
- 不同的Actor可以同時執行它們的消息。
對於第1點至今還有一些爭論,例如Actor是否可以並行執行它的消息,Actor是否應該保證執行順序與消息到達的一致(祥見Wikipedia的相關詞條)。而第2點是毋庸置疑的,因此Actor模型天生就帶有強大的並發特性。我們知道,系統中執行任務的最小單元是線程,數量一定程度上是有限的,而過多的線程會占用大量資源,也無法帶來最好的運行效率,因此真正在同時運行的Actor就會少很多。不過,這並不影響我們從概念上去理解“同一時刻可能有成千上萬個Actor正在運行”這個觀點。在這里,“正在運行”的含義是“處於運行狀態”。
Actor模型的使用無處不在,即使有些地方並沒有明確說采用的Actor模型:
- Google提出的Map/Reduce分布式運算平台
- C#,Java等語言中的lock互斥實現
- 傳統Email信箱的實現
- ……
Actor模型的現有實現
提到Actor模型的實現就不得不提Erlang。Erlang專以Actor模型為准則進行設計,它的每個Actor被稱作是“進程(Process)”,而進程之間唯一的通信方式便是相互發送消息。一個進程要做的,其實只是以下三件事情:
- 創建其他進程
- 向其他進程發送消息
- 接受並處理消息
例如《Programming Erlang》中的一段代碼:
loop() ->
receive
{From, {store, Key, Value}} ->
put(Key, {ok, Value}),
From ! {kvs, true},
loop();
{From, {lookup, Key}} ->
From ! {kvs, get(Key)},
loop()
end.
在Erlang中,大寫開頭的標識表示“變量(variable)”,而小寫開頭的標識表示“原子(atom)”,而大括號及其內部以逗號分割的數據結構,則被稱作是“元組(tuple)”。以上代碼的作用為一個簡單的“名字服務(naming service)”,當接受到{From, {store, Key, Value}}的消息時,則表示從From這個進程發來一個store請求,要求把Value與Key進行映射。而接受到{From, {lookup, Key}}消息時,則表示從From這個進程發來一個請求,要求返回Key所對應的內容。服務本身,也是通過向消息來源進程(即From)發送消息來進行回復的。
從Erlang語言的設計並不復雜,其類型系統更加幾乎可以用“簡陋”來形容,這使得其抽象能力十分欠缺,唯一的復雜數據結構似乎只有“元組”一種而已——不過我們現在不談其缺陷,談其“優勢”。Erlang語言設計的最大特點便是引入了“模式匹配(pattern matching)”,當且僅當受到的消息匹配了我們預設的結構(例如上面的{XXX, {store, YYY, ZZZ}}),則會進入相應的邏輯片斷。其次便是其尾遞歸的特性,可見上面的代碼中在loop方法的結尾再次調用了loop方法。
如果說Erlang語言專為Actor模型而設計,那么Scala語言(學Java的朋友們都去學Scala吧,那才是發展方向)中內置的Actor類庫則是外部語言Actor模型實現的經典案例了:
class Pong extends Actor {
def act() {
var pongCount = 0
while (true) {
receive {
case Ping =>
if (pongCount % 1000 == 0)
Console.println("Pong: ping " + pongCount)
sender ! Pong
pongCount = pongCount + 1
case Stop =>
Console.println("Pong: stop")
exit()
}
}
}
}
Pong類繼承了Actor模型,並覆蓋其act方法。由於沒有Erlang的尾遞歸特性,Scala Actor使用一個while (true)進行不斷的循環。獲取到消息之后,將會使用case語句對消息進行判斷,並執行相應邏輯。Scala的Actor類庫充分利用了Scala的語法特性,讓Actor模型好像是Scala內置功能一樣,非常漂亮。
此外,其他較為著名的Actor模型實現還有Io Language、Jetlang、以及.NET平台下的MS CCR和Retlang。后文中我們還會簡單提到.NET下Actor Model實現,其他內容就需要感興趣的朋友們自行挖掘了。
Actor模型中的任務調度
Actor模型的任務調度方式分為“基於線程(thread-based)的調度”以及“基於事件(event-based)的調度”兩種。
基於線程的調度為每個Actor分配一個線程,在接受一個消息(如在Scala Actor中使用receive)時,如果當前Actor的“郵箱(mail box)”為空,則會阻塞當前線程直到獲得消息為止。基於線程的調度實現起來較為簡單,例如在.NET中可以通過Monitor.Wait/Pulse來輕松實現這樣的生產/消費邏輯。不過基於線程的調度缺點也是非常明顯的,由於線程數量受到操作系統的限制,把線程和Actor捆綁起來勢必影響到系統中可以同時的Actor數量。而線程數量一多也會影響到系統資源占用以及調度,而在某些情況下大部分的Actor會處於空閑狀態,而大量阻塞線程既是系統的負擔,也是資源的浪費。因此基於線程的調度是一個擁有重大缺陷的實現,現有的Actor Model大都不會采取這種方式。
於是另一種Actor模型的任務調度方式便是基於事件的調度。“事件”在這里可以簡單理解為“消息到達”事件,而此時才會為Actor的任務分配線程並執行。很容易理解,我們現在便可以使用少量的線程來執行大量Actor產生的任務,既保證了運算資源的充分占用,也不會讓系統在同時進行的太多任務中“疲憊不堪”,這樣系統便可以得到很好的伸縮性。在Scala Actor中也可以選擇使用“react”而不是“recive”方法來使用基於事件的方式來執行任務。
現有的Actor Model一般都會使用基於事件的調度方式。不過某些實現,如MS CCR、Retlang、Jetlang等類庫還需要客戶指定資源分配方式,顯式地指定Actor與資源池(即線程池)之間的對應關系。而如Erlang或Scala則隱藏了這方面的分配邏輯,由系統整體進行統一管理。前者與后者相比,由於進行了更多的人工干涉,其資源分配可以更加合理,執行效率也會更高——不過其缺點也很明顯:會由此帶來額外的復雜度。
我們即將實現的簡單Actor Model類庫,也將使用了基於事件的調度方式。同樣為了簡化資源分配的過程,我們將直接使用.NET自帶的線程池來運行任務。
任務分配邏輯
如上文所述,這次要實現的是一個非常簡單的Actor模型,使用基於事件的分配方式,直接把任務交給.NET自帶的線程池去使用。不過我們又該什么時候把一個Actor推入線程池的執行隊列呢?這其實取決於我們執行Actor的兩個“基本原則”:
- 如果Actor的郵箱中包含消息,那么要盡早執行。
- 對於單個Actor對象來說,它的消息是順序執行的。
因此,我們有兩個“時機”可以把一個Actor交由線程池去執行:
- 當Actor接收到一個消息(且該Actor處於“等待”狀態)
- 當Actor執行完一個消息(且Actor的郵箱中存在更多消息)
顯然,在進行操作時需要小心處理並發造成的問題,因為一個“執行完”和多個“接受到”事件可能同時出現。如果操作不當,則容易出現各種錯誤的情況:
- 某個Actor的郵箱未空,卻已停止執行。
- 同一個Actor的兩個消息被並行地處理。
- Actor的郵箱已經沒有消息,卻被要求再次執行。
至於並行控制的方式,就請關注下面的實現吧。
簡單的Actor模型實現
Actor模型中最關鍵的莫過於Actor對象的實現。一個Actor的功能有如下三種:
- 將消息放入郵箱
- 接受並處理消息
- 循環/退出循環
因此Actor抽象類對外的接口大致如下:
public abstract class Actor<T> : IActor
{
protected abstract void Receive(T message);
protected void Exit() { ... }
public void Post(T message) { ... }
}
三個方法的簽名應該已經充分說明了各自的含義。不過IActor又是什么呢?請看它的定義:
internal interface IActor
{
void Execute();
bool Existed { get; }
int MessageCount { get; }
ActorContext Context { get; }
}
這是一個internal修飾的類型,這意味着它的訪問級別被限制在程序集內部。IActor接口的作用是作為一個統一的類型,交給Dispatcher——也就是Actor模型的任務分發邏輯所使用的。IActor接口的前三個成員很容易從名稱上理解其含義,那么ActorContext又是做什么用的呢?
internal class ActorContext
{
public ActorContext(IActor actor)
{
this.Actor = actor;
}
public IActor Actor { get; private set; }
...
}
public abstract class Actor<T> : IActor
{
protected Actor()
{
this.m_context = new ActorContext(this);
}
private ActorContext m_context;
ActorContext IActor.Context
{
get
{
return this.m_context;
}
}
...
}
在多線程的環境中,進行一些同步控制是非常重要的事情。線程同步的常用手段是lock,不過如果要減小鎖的粒度,那么勢必會使用Interlocked類下的CAS等原子操作,而那些操作只能針對最基礎的域變量,而不能針對經過封裝的屬性或方法等成員。ActorContext便包含了用於同步控制,以及其他直接表示Actor內部狀態各種字段的對象。這樣,我們便可以通過ActorContext對象來實現一個Lock-Free的鏈表或隊列。您可以會說,那么為什么要用獨立的ActorContext類型,而不直接把字段放置在統一的基類(例如ActorBase)中呢?這有兩點原因,第一點是所謂的“統一控制”便於管理,而第二點才是更為關鍵的:后文會涉及到F#對這Actor模型的使用,只可惜F#在對待父類的internal成員時有一個bug,因此不得不把相關實現替換成接口(IActor)。不過這不是本文的主題,我們下次再討論F#的問題。
ActorContext目前只有一個字段——沒錯,只需要一個,這個字段便是表示狀態的m_status。
internal class ActorContext
{
...
public const int WAITING = 0;
public const int EXECUTING = 1;
public const int EXITED = 2;
public int m_status;
}
m_status字段的類型為int,而不是枚舉,這是為了可以使用Interlocked中的CAS操作。而對這個狀態的操作,也正好形成了我們同步操作過程中的“壁壘”。我們的每個Actor在任意時刻都處於三種狀態之一:
- 等待(Waiting):郵箱為空,或剛執行完一個消息,正等待分配任務。
- 執行(Executing):正在執行一個消息(確切地說,由於線程池的緣故,它也可能是還在隊列中等待,不過從概念上理解,我們認為它“已經”執行了)。
- 退出(Exited):已經退出,不會再執行任何消息。
顯然,只有當m_status為WAITING時才能夠為Actor分配運算資源(線程)以便執行,而分配好資源(將其推入.NET線程池)之后,它的狀態就要變成EXECUTING。這恰好可以用一個原子操作形成我們需要的“壁壘”,可以讓多個“請求”,“有且只有一個”成功,即“把Actor的執行任務塞入線程池”。如下:
internal class Dispatcher
{
...
public void ReadyToExecute(IActor actor)
{
if (actor.Existed) return;
int status = Interlocked.CompareExchange(
ref actor.Context.m_status,
ActorContext.EXECUTING,
ActorContext.WAITING);
if (status == ActorContext.WAITING)
{
ThreadPool.QueueUserWorkItem(this.Execute, actor);
}
}
...
}
CompareExchange方法返回這次原子操作前m_status的值,如果它為WAITING,那么這次操作(也僅有這次操作)成功地將m_status修改為EXECUTING。在這個情況下,Actor將會被放入線程池,將會由Execute方法來執行。從上述實現中我們可以發現,這個方法在多線程的情況下也能夠正常工作。那么ReadyToExecute方法該在什么地方被調用呢?應該說是在任何“可能”讓Actor開始執行的時候得到調用。按照文章開始的說法,其中一個情況便是“當Actor接收到一個消息時”:
public abstract class Actor<T> : IActor
{
...
private Queue<T> m_messageQueue = new Queue<T>();
...
public void Post(T message)
{
if (this.m_exited) return;
lock (this.m_messageQueue)
{
this.m_messageQueue.Enqueue(message);
}
Dispatcher.Instance.ReadyToExecute(this);
}
}
而另一個地方,自然是消息“執行完畢”,且Actor的郵箱中還擁有消息的時候,則再次為其分配運算資源。這便是Dispatcher.Execute方法的邏輯:
public abstract class Actor<T> : IActor
{
...
bool IActor.Existed
{
get
{
return this.m_exited;
}
}
int IActor.MessageCount
{
get
{
return this.m_messageQueue.Count;
}
}
void IActor.Execute()
{
T message;
lock (this.m_messageQueue)
{
message = this.m_messageQueue.Dequeue();
}
this.Receive(message);
}
private bool m_exited = false;
protected void Exit()
{
this.m_exited = true;
}
...
}
internal class Dispatcher
{
...
private void Execute(object o)
{
IActor actor = (IActor)o;
actor.Execute();
當程序執行到此處時,actor的Execute方法已經從郵箱尾部獲取了一條消息,並交由用戶實現的Receive方法執行。同時,Actor的Exit方法也可能被調用,使它的Exited屬性返回true。不過到目前為止,因為ActorContext.m_status一直保持為EXECUTING,因此這段時間中任意新消息所造成的ReadyToExecute方法的調用都不會為Actor再次分配運算資源。不過接下來,我們將會修改m_status,這可能會造成競爭。那么我們又該怎么處理呢?
如果用戶調用了Actor.Exit方法,那么它的Exited屬性則會返回true,我們可以將m_status設為EXITED,這樣Actor再也不會回到WAITING狀態,也就避免了無謂的資源分配:
if (actor.Existed)
{
Thread.VolatileWrite(
ref actor.Context.m_status,
ActorContext.EXITED);
}
else
{
如果Actor沒有退出,那么它會被短暫地切換為WAITING狀態。此后如果Actor的郵箱中存在剩余的消息,那么我們會再次調用ReadyToExecute方法“嘗試”再次為Actor分配運算資源:
Thread.VolatileWrite(
ref actor.Context.m_status,
ActorContext.WAITING);
if (actor.MessageCount > 0)
{
this.ReadyToExecute(actor);
}
}
}
}
顯然,在VolatileWrite和ReadyToExecute方法之間,可能會到來一條新的消息,因而再次引發一次並行地ReadyToExecute調用。不過根據我們之前的分析,這樣的競爭並不會造成問題,因此在這方面我們可以完全放心。
至此,我們已經完整地實現了一個簡單的Actor模型,邏輯清晰,功能完整——而這一切,僅僅用了不到150行代碼。不用懷疑,這的確是事實。
使用示例
Actor模型的關鍵在於消息傳遞形式(Message Passing Style)的工作方式,通信的唯一手段便是傳遞消息。在使用我們的Actor模型之前,我們需要繼承Actor<T>類來構建一個真正的Actor類型。例如一個最簡單的計數器:
public class Counter : Actor<int>
{
private int m_value;
public Counter() : this(0) { }
public Counter(int initial)
{
this.m_value = initial;
}
protected override void Receive(int message)
{
this.m_value += message;
if (message == -1)
{
Console.WriteLine(this.m_value);
this.Exit();
}
}
}
當計數器收到-1以外的數值時,便會累加到它的計數器上,否則便會打印出當前的值並退出。這里無需做任何同步方面的考慮,因為對於單個Actor來說,所有的消息都是依次處理,不會出現並發的情況。Counter的使用自然非常簡單:
static void Main(string[] args)
{
Counter counter = new Counter();
for (int i = 0; i < 10000; i++)
{
counter.Post(i);
}
counter.Post(-1);
Console.ReadLine();
}
不過您可能會問,這樣的調用又有什么作用,又能實現什么呢?您現在可以去網上搜索一些Actor模型解決問題的示例,或者您可以等待下一篇文章中,我們使用F#來操作這個Actor模型。您會發現,配合F#的一些特性,這個Actor模型會變得更加實用,更為有趣。
此外,在下一篇文章里我們也會對這個Actor模型進行簡單的性能分析。如果您要把它用在生產環境中,那么可能還需要對它再進行一些細微地調整。
C#使用Actor模型的缺陷
在Erlang中,每個消息都使用模式匹配來限制其“結構”或“格式”,以此表達不同含義。C#類型系統的抽象能力遠勝於Erlang,但是Erlang的“動態性”使得開發人員可以在程序中隨意發送和接收任何類型,這種“自由”為Erlang帶來了靈活。我們的Actor模型中,每個Actor對象都需要一種特定的消息格式,而這種消息格式承擔了“表現Actor所有職責”的重任,但是一個Actor的職責是可能由任何數據組合而成。例如一段最簡單的“聊天”程序,其Actor表示了一個“人”,用Erlang實現可能就會這么寫:
loop() ->
receive
% 系統要求發起聊天,於是向對方打招呼
{start, Person} ->
Person ! {self(), {greeting, "你好")},
loop();
% 有人前來發起聊天,於是向對方說了點什么
{Person, {greeting, Message}} ->
Person ! {self(), {say, "..."}},
loop();
% 有人前來說話,於是拜拜
{Person, {say, Message}} ->
Person ! {self(), {bye, "..."}},
loop();
...
end.
不同的元組(tuple)配合不同的原子(atom)便表示了一條消息的“含義”,但是使用C#您又該怎樣來表現這些“命令”呢?您可能會使用:
- 使用object[]作為消息類型,並檢查其元素。
- 使用object作為消息類型,並判斷消息的具體類型。
- 使用枚舉或字符串代表“命令”,配合一個參數集合。
第1種做法十分麻煩;第2種則需要“先定義,后使用”也頗為不易;而第3種做法,平心而論,如果有一個“分發類庫”的支持就會比較理想——可能比這篇文章中的F#還要理想。老趙正在努力實現這一功能,因為C#的這個特性會影響到.NET平台下所有Actor模型(如第一篇文章中所提到的CCR或Retlang)的使用。
而目前,我們先來看看F#是否可以略為緩解一下這方面的問題。
在F#中使用Actor模型
Erlang沒有嚴謹的類型系統,其“消息類型”是完全動態的,因此非常靈活。那么F#又有什么“法寶”可以解決C#中所遇到的尷尬呢?在現在這個問題上,F#有三個領先於C#的關鍵:
- 靈活的類型系統
- 強大的模式匹配
- 自由的語法
雖然F#也是強類型的編譯型語言(這點和C#一致),但是F#的類型系統較C#靈活許多,例如在“聊天”這個示例中,我們就可以編寫如下類型作為“消息”類型:
type Message = string
type ChatMsg =
| Start of Person
| Greeting of Person * Message
| Say of Person * Message
| Bye of Person * Message
在這個定義中用到了F#類型系統中的三個特點:
- 類型別名:即type Message = string。為一個已有的類型定義一個別名,可以得到更好的語義。與C#使用using定義別名不同的是,F#中的別名可以定義為全局性的,而不僅僅是“源代碼”級別的別名。
- Discriminated Unions:即type ChatMsg = …。Discriminated Unions可以為一個類型指定多個discriminator,每個discriminator由一個名稱,以及另一種具體類型來表示。不同的discriminator的具體類型可以不同。
- 元組(Tuple):即Person * Message。在F#中可以通過把現有類型按順序進行任意組合來得到新的類型,這種類型便被稱為“元組”。
在Actor模型中,我們便組合了F#的三個特別特性,定義了消息的具體類型。而在使用時,我們便可以使用“模式匹配”對不同的“消息”——其實是CharMsg的不同discriminator進行不同地處理。於是具體的Actor類型Person,便可以使用如下定義:
and Person(name: string) =
inherit ChatMsg Actor()
let GetRandom =
let r = new Random(DateTime.Now.Millisecond)
fun() -> r.NextDouble()
member self.Name = name
override self.Receive(message) =
match (message) with
Person類的構造函數接受一個name作為參數,並將其放置到Name屬性中。我們同時定義了GetRandom函數,它會在內部構造一個System.Random對象,並每次返回NextDouble方法的值(請注意,無論調用多少次GetRandom方法,永遠使用了同一個Random對象,因為他是在定義GetRandom方法時創建的)。而在override的Receive方法中,我們使用“模式匹配”對message對象進行處理:
// 系統要求發起聊天
| Start(p) ->
Console.WriteLine("系統讓{0}向{1}打招呼", self.Name, p.Name)
Greeting(self, "Hi, 有空不?") |> p.Post
請注意上述最后一行,原本我們使用p.Post(…)的調用方式,現在使用了“|>”符號代替。在F#中,x |> f便代表了f(x),它的本意是可以把f(g(h(x)))這樣冗余的調用方式轉變為清晰的“消息發送”形式:x |> h |> g |> f。而“消息發送”也恰好是我們所需要的“感覺”。因此,我們在接下來的代碼中也使用這樣的方式:
// 打招呼
| Greeting(p, msg) ->
Console.WriteLine("{0}向{1}打招呼:{2}", p.Name, self.Name, msg)
if (GetRandom() < 0.8) then
Say(self, "好,聊聊。") |> p.Post
else
Bye(self, "沒空,bye!") |> p.Post
// 進行聊天
| Say(p, msg) ->
Console.WriteLine("{0}向{1}說道:{2}", p.Name, self.Name, msg)
if (GetRandom() < 0.8) then
Say(self, "繼續聊。") |> p.Post
else
Bye(self, "聊不動了,bye!") |> p.Post
// 結束
| Bye(p, msg) ->
Console.WriteLine("{0}向{1}再見:{2}", p.Name, self.Name, msg)
至此,Person類型定義完畢。我們構造三個Person對象,讓它們隨意聊天:
let startChat() =
let p1 = new Person("Tom")
let p2 = new Person("Jerry")
let p3 = new Person("老趙")
Start(p2) |> p1.Post
Start(p3) |> p2.Post
startChat()
結果如下(內容會根據隨機結果不同而有所改變):
系統讓Tom向Jerry打招呼 系統讓Jerry向老趙打招呼 Jerry向老趙打招呼:Hi, 有空不? Tom向Jerry打招呼:Hi, 有空不? Jerry向Tom說道:好,聊聊。 老趙向Jerry說道:好,聊聊。 Jerry向老趙說道:繼續聊。 Tom向Jerry說道:繼續聊。 Jerry向Tom說道:繼續聊。 老趙向Jerry說道:繼續聊。 Jerry向老趙說道:繼續聊。 Tom向Jerry再見:聊不動了,bye! 老趙向Jerry說道:繼續聊。 Jerry向老趙再見:聊不動了,bye!
使用Actor模型抓取網絡數據
我們再來看一個略為“現實”一點的例子,需要多個Actor進行配合。首先,我們定義一個“抓取”數據用的Actor,它的唯一作用便是接受一個消息,並將抓取結果傳回:
type Crawler() =
inherit ((obj Actor) * string) Actor()
override self.Receive(message) =
let (monitor, url) = message
let content = (new WebClient()).DownloadString(url)
(url, content) |> monitor.Post
再使用“單件”方式直接定義一個monitor對象:
let monitor =
{ new obj Actor() with
override self.Receive(message) =
match message with
// crawling
| :? string as url -> (self, url) |> (new Crawler()).Post
// get crawled result
| :? (string * string) as p ->
let (url, content) = p
Console.WriteLine("{0} => {1}", url, content.Length)
// unrecognized message
| _ -> failwith "Unrecognized message" }
每次收到“抓取”消息時,monitor都會創建一個Crawler對象,並把url發送給它,並等待回復消息。而在使用時,只要把對象一個一個“發送”給monitor便可:
let urls = [
"http://www.live.com";
"http://www.baidu.com";
"http://www.google.com";
"http://www.cnblogs.com";
"http://www.microsoft.com"]
List.iter monitor.Post urls
運行結果如下:
http://www.live.com => 18035 http://www.google.com => 6942 http://www.cnblogs.com => 62688 http://www.microsoft.com => 1020 http://www.baidu.com => 3402
性能分析
最后,我們再對這個Actor模型的性能作一點簡單的分析。
如果從“鎖”的角度來說,這個Actor模型唯一的鎖是在消息隊列的訪問上,這基本上就是唯一的瓶頸。如果把它替換為lock-free的隊列,那么整個Actor模型就是完全的lock-free實現,其“調度”性能可謂良好。
不過,從另一個角度來說,這個Actor模型的調度非常頻繁,每次只執行一個消息。試想,如果執行一個消息只需要50毫秒,而進行一次調度就需要100毫秒,那么這個性能的瓶頸還是落在“調度”上。因此,如果我們需要進一步提高Actor模型的性能,則需要從Dispatcher.Execute方法上做文章,例如把每次執行一個消息修改為每次執行n個消息,或超過一個時間的閾值再進行下一次調度。減少調度,也是提高Actor模型性能的關鍵之一。
此外,如果覺得.NET自帶的線程池性能不高,或者說會受到程序其他部分的影響,那么也可以使用獨立的線程池進行替換。
自然,任何性能優化都不能只憑感覺下手,一切都要用數據說話,因此在優化時一定要先建立合適的Profile機制,保證每一步優化都是有效的。
