现今,单台机器拥有多个独立的计算单元已经太常见了,这点在服务器的处理器上表现尤为明显,据AMD的一张2012-2013服务器路线图显示,服务器处理器的核心数将在2013年达到20颗之多,合理的利用CPU资源已是一个不得不考虑的问题。
不少C++程序员依然使用着多线程模型,但是对多线程的掌控并不是一件容易的事情,开发中容易出错、难以调试,有些开发者为了避免多线程带来的复杂度而弃用多线程,有些开发者则另投其他语言阵营,例如:Erlang,其实我们还有其他的选择,Theron就是其中之一。
1、什么是Theron?
Theron是一个用于并发编程的C++库,通过Theron我们可以避免多线程开发中各种痛处,例如:共享内存、线程同步,Theron通过Actor模型向我们展示了另一种思维。
2、什么是Actor模型?
Erlang因为其优秀的并发特性而被大家所关注,而其并发特性的关键之一就是在于其采用了Actor模型,与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并发执行。
3、Hello Theron
在谈及更多内容之前,我们先来看看Theron的一个简单的范例,借以获得一个最直观的印象,Theron提供了makefile便于gcc用户编译,同时其也为Windows用户提供了Visual Studio solution文件Theron.sln用于构建Theron。
编译Theron很容易,不会有太多的障碍,需要注意的是构建Theron需要指定依赖的线程库,Theron支持三种线程库:std::thread(C++11 标准线程库)、Boost.Thread和Windows threads。
使用makefile构建时,通过threads参数指定使用的线程库,使用Visual Studio构建时,通过选择适当的Solution configuration来指定使用的线程库,下面我们来看一个最简单的范例:
#include
#include
#include
// 定义一个消息类型
// 在 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("%sn", message.m_string);
if (!Send(message, from))
printf("Failed to send message to address %dn", from.AsInteger());
}
};
int main()
{
// Framework 对象用于管理 Actors
Theron::Framework framework;
// 通过 Framework 构建一个 Actor 实例并持有其引用
// Actor 的引用类似于 Java、C# 等语言中的引用的概念
// Theron::ActorRef 采用引用计数的方式实现,类似于 boost::shared_ptr
Theron::ActorRef simpleActor(framework.CreateActor());
// 创建一个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 --- %dn", message.m_string, GetAddress().AsInteger());
#ifdef _MSC_VER
Sleep(1000);
#else
sleep(1);
#endif
}
}
此Handler会不断的打印message并且带上当前Actor的地址信息,在main函数中,我们构建两个Actor实例并通过消息唤醒它们,再观察输出结果:
Hello Theron! --- 1
Hello Theron! --- 2
Hello Theron! --- 2
Hello Theron! --- 1
Hello Theron! --- 2
Hello Theron! --- 1
Hello Theron! --- 2
Hello Theron! --- 1
......
这和我们预期的一样,两个Actor实例在不同的线程下工作,实际上,Framework创建的时候会创建系统级的线程,默认情况下会创建两个(可以通过 Theron::Framework 构造函数的参数决定创建线程的数量),它们构成一个线程池,我们可以根据实际的CPU核心数来决定创建线程的数量,以确保CPU被充分利用。
那么,线程池的线程是以何种方式进行调度的呢?如下图所示:
接收到消息的Actor会被放置于一个线程安全的Work队列中,此队列中的Actor会被唤醒的工作线程取出,并进行消息的处理,这个过程中有两个需要注意的地方:
(1)、对于某个Actor我们可以为某个消息类型注册多个消息处理函数,那么,此消息类型对应的多个消息处理函数会按照注册的顺序被串行执行下去。
(2)、线程按顺序处理Actor收到的消息,一个消息未处理完成不会处理消息队列中的下一个消息我们可以想象,如果存在三个Actor,其中两个Actor的消息处理函数中存在死循环(例如上例中的while(true)),那么它们一旦执行就会霸占两条线程,若线程池中没有多余线程,那么另一个Actor将被“饿死”(永远得不到执行)。
我们可以在设计上避免这种 Actor 的出现,当然也可以适当的调整线程池的大小来解决此问题,Theron中,线程池中线程的数量是可以动态控制的,线程利用率也可以测量,但是务必注意的是,过多的线程必然导致过大的线程上下文切换开销。
4、一个详细的例子
我们再来看一个详细的例子,借此了解Theron带来的便利,生产者消费者的问题是一个经典的线程同步问题,我们来看看Theron如何解决这个问题:
#include
#include
#include
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 %dn", 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 %dn", 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());
Theron::ActorRef consumer(framework.CreateActor());
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;
}
生产者生产物品,消费者消费物品,它们并行进行,我们没有编写创建线程的代码,没有构建共享内存,也没有处理线程的同步,这一切都很轻松的完成了。
5、代价和设计
和传统的多线程程序相比Theron有不少优势,通过使用Actor,程序能够自动的并行执行,而无需开发者费心,Actor总是利用消息进行通信,消息必须拷贝,这也意味着我们必须注意到,在利用Actor进行并行运算的同时需避免大量消息拷贝带来的额外开销。
Actor模型强调了一切皆为Actor,这自然可以作为我们使用Theron的一个准则,但过多的Actor存在必然导致Actor间频繁的通信。
适当的使用Actor并且结合Object模型也许会是一个不错的选择,例如,我们可以对系统进行适当划分,得到一些功能相对独立的模块,每个模块为一个Actor,模块内部依然使用Object模型,模块间通过Actor的消息机制进行通信。
马海祥博客点评:
Theron是个有趣的东西,也许你没有使用过它,你也不了解Actor模型,但是Actor的思想却不新鲜,甚至你可能正在使用,目前来说,我还没有找到Theron在哪个实际的商业项目中使用,因此对Theron的使用还存在一些未知的因素。
还有一些特性,诸如跨主机的分布式的并行执行是Theron不支持的,这些都限制了Theron的使用,不过我也正在积极的改变一些东西,无论Theron未来如何,Theron以及Actor模型带来的思想会让我们更加从容面对多核的挑战。
本文发布于马海祥博客文章,如想转载,请注明原文网址摘自于http://www.mahaixiang.cn/bcyy/954.html,注明出处;否则,禁止转载;谢谢配合!