現今,單臺機器擁有多個獨立的計算單元已經太常見了,這點在服務器的處理器上表現尤為明顯,據 AMD 的一張 2012-2013 服務器路線圖顯示,服務器處理器的核心數將在 2013 年達到 20 顆之多。合理的利用 CPU 資源已是一個不得不考慮的問題。不少 C++ 程序員依然使用著多線程模型,但是對多線程的掌控并不是一件容易的事情,開發中容易出錯、難以調試。有些開發者為了避免多線程帶來的復雜度而棄用多線程,有些開發者則另投其他語言陣營,例如 Erlang。其實我們還有其他的選擇,Theron 就是其中之一。
Theron 是一個用于并發編程的 C++ 庫(http://www.theron-library.com/),通過 Theron 我們可以避免多線程開發中各種痛處,例如:共享內存、線程同步。Theron 通過 Actor 模型向我們展示了另一種思維。
Erlang 因為其優秀的并發特性而被大家所關注,而其并發特性的關鍵之一就是在于其采用了 Actor 模型(http://c2.com/cgi/wiki?ErlangLanguage)。與 Actor 模型相對應的模型則是我們在面向對象編程中使用的 Object 模型,Object 模型中宣揚,一切皆為 Object(對象),而 Actor 模型則認為一切皆為 Actor。Actor 模型中,Actor 之間通過消息相互通信,這是其和 Object 模型的一個顯著的區別,換而言之 Actor 模型使用消息傳遞機制來代替了 Object 模型中的成員方法調用。這樣做意義重大,因為相對于成員方法的調用來說,消息的發送是非阻塞的,它無需等待被調用方法執行完成就可以返回,下圖顯示了此種區別:
A::a() 調用了 objB.b(),此時 A::a() 必須等待 B::b() 的返回才能繼續執行。在 Actor 模型中,對應的做法是 Actor A 向 Actor B 發送消息并立即返回,這時候 Actor A 可以繼續執行下去,與此同時 Actor B 收到消息被喚醒并和 Actor A 并行執行下去。
Theron 中的每個 Actor 都會綁定一個唯一的地址,通過 Actor 的地址就可以向其發送消息了,每個 Actor 都有一個消息隊列。從編碼者的角度看來,每實例化一個 Actor 都創建了一個和 Actor 相關的“線程”(非系統級的線程)。每個 Actor 總是被單線程的執行。總結來說 Theron 的并發特性的關鍵就在于:每個 Actor 在屬于自己的單個“線程”中執行,而多個 Actor 并發執行。
相關廠商內容
相關贊助商
QCon北京2017,4月16-18日,北京·國家會議中心,精彩內容搶先看
在談及更多內容之前,我們先來看看 Theron 的一個簡單的范例,借以獲得一個最直觀的印象。在 http://www.theron-library.com/ 可以下載到 Theron 的最新版,Theron 提供了 makefile 便于 gcc 用戶編譯,同時其也為 Windows 用戶提供了 Visual Studio solution 文件 Theron.sln 用于構建 Theron。編譯 Theron 很容易,不會有太多的障礙,需要注意的是構建 Theron 需要指定依賴的線程庫,Theron 支持三種線程庫:std::thread(C++11 標準線程庫)、Boost.Thread 和 Windows threads。使用 makefile 構建時,通過 threads 參數指定使用的線程庫(更為詳細的信息參考:http://www.theron-library.com/index.php?t=page&p=gcc),使用 Visual Studio 構建時,通過選擇適當的 Solution configuration 來指定使用的線程庫(更為詳細的信息參考:http://www.theron-library.com/index.php?t=page&p=visual studio)。下面我們來看一個最簡單的范例:
#include <stdio.h>#include <Theron/Framework.h>#include <Theron/Actor.h>// 定義一個消息類型// 在 Theron 中,任何類型都可以作為一個消息類型// 唯一的一個約束是消息類型的變量能夠被拷貝的// 消息按值發送(而非發送它們的地址)struct StringMessage{ char m_string[64];};// 用戶定義的 Actor 總需要繼承于 Theron::Actor// 每個 Actor 和應用程序的其他部分通信的唯一途徑就是通過消息class Actor : public Theron::Actor{public: inline Actor() { // 注冊消息的處理函數 RegisterHandler(this, &Actor::Handler); }PRivate: // 消息處理函數的第一個參數指定了處理的消息的類型 inline void Handler(const StringMessage& message, const Theron::Address from) { printf("%s/n", message.m_string); if (!Send(message, from)) printf("Failed to send message to address %d/n", from.AsInteger()); } };int main(){ // Framework 對象用于管理 Actors Theron::Framework framework; // 通過 Framework 構建一個 Actor 實例并持有其引用 // Actor 的引用類似于 java、C# 等語言中的引用的概念 // Theron::ActorRef 采用引用計數的方式實現,類似于 boost::shared_ptr Theron::ActorRef simpleActor(framework.CreateActor<Actor>()); // 創建一個 Receiver 用于接收 Actor 發送的消息 // 用于在非 Actor 代碼中(例如 main 函數中)與 Actor 通信 Theron::Receiver receiver; // 構建消息 StringMessage message; strcpy(message.m_string, "Hello Theron!"); // 通過 Actor 的地址,我們就可以向 Actor 發送消息了 if (!framework.Send(message, receiver.GetAddress(), simpleActor.GetAddress())) printf("Failed to send message!/n"); // 等到 Actor 發送消息,避免被關閉主線程 receiver.Wait(); return 0;}這個范例比較簡單,通過 Actor 輸出了 Hello Theron。需要額外說明的一點是消息在 Actor 之間發送時會被拷貝,接收到消息的 Actor 只是引用到被發送消息的一份拷貝,這么做的目的在于避免引入共享內存、同步等問題。
Theron 的消息處理
前面談到過,每個 Actor 都工作在一個屬于自己的“線程”上,我們通過一個例子來認識這一點,我們修改上面例子中的 Actor:: Handler 成員方法:
inline void Handler(const StringMessage& message, const Theron::Address from) { while (true) { printf("%s --- %d/n", message.m_string, GetAddress().AsInteger());#ifdef _MSC_VER Sleep(1000);#else sleep(1);#endif }}此 Handler 會不斷的打印 message 并且帶上當前 Actor 的地址信息。在 main 函數中,我們構建兩個 Actor 實例并通過消息喚醒它們,再觀察輸出結果:
Hello Theron! --- 1Hello Theron! --- 2Hello Theron! --- 2Hello Theron! --- 1Hello Theron! --- 2Hello Theron! --- 1Hello Theron! --- 2Hello Theron! --- 1......這和我們預期的一樣,兩個 Actor 實例在不同的線程下工作。實際上,Framework 創建的時候會創建系統級的線程,默認情況下會創建兩個(可以通過 Theron::Framework 構造函數的參數決定創建線程的數量),它們構成一個線程池,我們可以根據實際的 CPU 核心數來決定創建線程的數量,以確保 CPU 被充分利用。線程池的線程是以何種方式進行調度的?如下圖:
接收到消息的 Actor 會被放置于一個線程安全的 Work 隊列中,此隊列中的 Actor 會被喚醒的工作線程取出,并進行消息的處理。這個過程中有兩個需要注意的地方:
對于某個 Actor 我們可以為某個消息類型注冊多個消息處理函數,那么此消息類型對應的多個消息處理函數會按照注冊的順序被串行執行下去線程按順序處理 Actor 收到的消息,一個消息未處理完成不會處理消息隊列中的下一個消息 我們可以想象,如果存在三個 Actor,其中兩個 Actor 的消息處理函數中存在死循環(例如上例中的 while(true)),那么它們一旦執行就會霸占兩條線程,若線程池中沒有多余線程,那么另一個 Actor 將被“餓死”(永遠得不到執行)。我們可以在設計上避免這種 Actor 的出現,當然也可以適當的調整線程池的大小來解決此問題。Theron 中,線程池中線程的數量是可以動態控制的,線程利用率也可以測量。但是務必注意的是,過多的線程必然導致過大的線程上下文切換開銷。一個詳細的例子
我們再來看一個詳細的例子,借此了解 Theron 帶來的便利。生產者消費者的問題是一個經典的線程同步問題,我們來看看 Theron 如何解決這個問題:
#include <stdio.h>#include <Theron/Framework.h>#include <Theron/Actor.h>const int PRODUCE_NUM = 5;class Producer : public Theron::Actor{public: inline Producer(): m_item(0) { RegisterHandler(this, &Producer::Produce); }private: // 生產者生產物品 inline void Produce(const int& /* message */, const Theron::Address from) { int count(PRODUCE_NUM); while (count--) { // 模擬一個生產的時間#ifdef _MSC_VER Sleep(1000);#else sleep(1);#endif printf("Produce item %d/n", m_item); if (!Send(m_item, from)) printf("Failed to send message!/n"); ++m_item; } } // 當前生產的物品編號 int m_item;};class Consumer : public Theron::Actor{public: inline Consumer(): m_consumeNum(PRODUCE_NUM) { RegisterHandler(this, &Consumer::Consume); }private: inline void Consume(const int& item, const Theron::Address from) { // 模擬一個消費的時間#ifdef _MSC_VER Sleep(2000);#else sleep(2);#endif printf("Consume item %d/n", item); --m_consumeNum; // 沒有物品可以消費請求生產者進行生產 if (m_consumeNum == 0) { if (!Send(0, from)) printf("Failed to send message!/n"); m_consumeNum = PRODUCE_NUM; } } int m_consumeNum;};int main(){ Theron::Framework framework; Theron::ActorRef producer(framework.CreateActor<Producer>()); Theron::ActorRef consumer(framework.CreateActor<Consumer>()); if (!framework.Send(0, consumer.GetAddress(), producer.GetAddress())) printf("Failed to send message!/n"); // 這里使用 Sleep 來避免主線程結束 // 這樣做只是為了簡單而并不特別合理 // 在實際的編寫中,我們應該使用 Receiver#ifdef _MSC_VER Sleep(100000);#else sleep(100);#endif return 0;}生產者生產物品,消費者消費物品,它們并行進行,我們沒有編寫創建線程的代碼,沒有構建共享內存,也沒有處理線程的同步。這一切都很輕松的完成了。
代價和設計
和傳統的多線程程序相比 Theron 有不少優勢,通過使用 Actor,程序能夠自動的并行執行,而無需開發者費心。Actor 總是利用消息進行通信,消息必須拷貝,這也意味著我們必須注意到,在利用 Actor 進行并行運算的同時需避免大量消息拷貝帶來的額外開銷。
Actor 模型強調了一切皆為 Actor,這自然可以作為我們使用 Theron 的一個準則。但過多的 Actor 存在必然導致 Actor 間頻繁的通信。適當的使用 Actor 并且結合 Object 模型也許會是一個不錯的選擇,例如,我們可以對系統進行適當劃分,得到一些功能相對獨立的模塊,每個模塊為一個 Actor,模塊內部依然使用 Object 模型,模塊間通過 Actor 的消息機制進行通信。
Theron 的未來
Theron 是個有趣的東西,也許你沒有使用過它,你也不了解 Actor 模型,但是 Actor 的思想卻不新鮮,甚至你可能正在使用。目前來說,我還沒有找到 Theron 在哪個實際的商業項目中使用,因此對 Theron 的使用還存在一些未知的因素。還有一些特性,諸如跨主機的分布式的并行執行是 Theron 不支持的,這些都限制了 Theron 的使用,不過作者正在積極的改變一些東西(例如,作者表示會在今后添加 Remote Actors)。無論 Theron 未來如何,Theron 以及 Actor 模型帶來的思想會讓我們更加從容面對多核的挑戰。
作者簡介
梁國棟,熱愛編程,熱衷于撰寫技術類文章,精益思想倡導者,UNIX 哲學實踐者,專注于高性能服務器程序的研發多年,目前負責網游服務器的研發工作。
感謝李永倫對本文的審校。
給InfoQ中文站投稿或者參與內容翻譯工作,請郵件至editors@cn.infoq.com。也歡迎大家通過新浪微博(@InfoQ)或者騰訊微博(@InfoQ)關注我們,并與我們的編輯和其他讀者朋友交流。
新聞熱點
疑難解答