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