65.9K
CodeProject 正在变化。 阅读更多。
Home

使用 Actor 编程模型

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.78/5 (8投票s)

2012年10月23日

CPOL

7分钟阅读

viewsIcon

55576

downloadIcon

490

使用 Actor 编程模型

引言

随着行业日趋成熟,软件开发者们开始逐渐认识到并发和并行处理的重要性。我最近详细研究了 Actor 编程模型,并对其简洁性和健壮性深感着迷。Actor 编程模型是一种并发编程技术,它提供了一种强大的机制来封装多种并发特性。基于该模型构建代码,可以用相当简单的代码实现一些高级并发结构。对于那些在并行编程方面经验不足的开发者来说,他们只要遵循几个规则,就能编写出避免常见竞态条件和非确定性行为问题的并发代码。

本文将探讨在 C++ 中使用 Win32 的 Actor 的一些用法。提供的代码示例是在 Visual Studio 2010 中开发和测试的,但我预期它们在较新或较旧版本的编译器以及 64 位环境中也能正常工作。最新的 C++ 2011 语言标准为该语言添加了一些新的线程语法(从 Visual Studio 2012 开始实现),这些语法提供了内置的 C++ 功能,可以实现某些线程细节的替代方案。代码是 Win32 的,但底层原理是通用的,适用于许多语言和平台。

背景  

总的来说,Actor 的概念大约在 1973 年(1)被开发和完善,并在一个多处理器网络平台上得到了发展。在多处理器机器上实现 Actor 提供了几种基本的并发特性,包括并行同步的封装和串行消息处理,这使得分叉/合并、异步/等待、管道处理等更高级别的并发特性得以实现。Actor 代码封装了线程和同步管理,因此派生自它的类可以在不实现底层细节的情况下使用线程技术。

它是什么? 

最简单的说,Actor 是一个具有以下特征的对象: 

  • 它是一个自治的、交互式的并行系统组件,包含一个执行控制上下文(例如,进程、线程或协程)、一个外部可访问的地址、可变的本地状态以及用于操作和观察状态的 API。(2) 每个 Actor 的状态都是唯一的,不与其他对象共享。
  • 处理状态包括:已创建、运行中、已停止,以及由程序员确定的子状态。对于所有处理状态,外部代码可以查看 Actor 的内部细节并检索关于 Actor 的状态信息,具体取决于 Actor 的允许或禁止。Actor 的生命周期从创建到运行,再到停止。一旦停止,它就不会重新启动。(3)

  • 它具有用于启动处理和管理同步消息队列的 API, Actor 从该队列接收来自封闭程序(包括其自身或其他 Actor)的操作请求。当 Actor 被创建时,队列可以接受消息,但消息不会被处理。当 Actor 运行时,它会按顺序、原子地处理消息,一次一个。待处理的消息会排队在消息队列中。当 Actor 停止时,消息将被忽略。来自多个执行上下文发送给 Actor 的消息不能保证按时间顺序到达,尽管来自同一来源的多个消息会按时间顺序到达。(4)

  • Actor 由外部创建,并通过外部 API 调用启动。它通过消息队列发送“停止”请求来停止,Actor 通过清理和终止自身来响应。运行时,Actor 可以处理有限数量的消息,向自身或其他 Actor 发送消息,更改本地状态,并创建/控制/终止有限数量的其他 Actor 对象。除了创建和启动,本地状态仅在处理消息时发生变异。
  • Actor 是一个被动且惰性的对象。除非通过消息队列向其发送消息,否则它不会响应或执行。
  • 多个 Actor 并发处理消息时,可以看到并行性。

此处创建的示例将 Actor 视为一个 C++“框架”基类,其中包含基本功能,以及一个或多个派生类,这些派生类包含一些必需的底层机制和程序员提供的所需行为。有关上述 Actor 表示法的说明,请参见图 1 中的图表。 

 

图 1. Actor 编程模型中基类的表示

创建后,Actor 基类提供了两个公共方法 `Start()` 和 `Send()`,用于启动 Actor 和发送消息到消息队列,外加一个受保护的方法 `Process()` 用于实现负载行为,以及一个 `Exit()` 方法用于终止。`Process()` 方法是纯虚函数,必须在派生类中实现。基类封装了消息处理以及线程的创建、删除和管理。派生类(由您提供)必须在 `Process()` 方法中实现主要的 Actor 行为和激活终止处理。这是一个最小描述;当然,程序员可以自行决定实现其他细节,例如检索 Actor 状态或内部数据。请注意,Actor 不能直接通过外部 API 调用(优雅地)停止,而是通过消息队列处理预先安排的关闭消息时异步地自行关闭并退出。确保这种行为是派生类和调用代码的责任。

Actor 基类的一个摘录如下: 

class HBActor
{
public:
  HBActor();
  virtual ~HBActor();

public:
  virtual void Send(BaseMessage* message);
  virtual void Start();

protected:
  virtual void Process(BaseMessage* /* msg */) = 0;

// ...
};

通过实现一些相当简单的类,我们可以轻松地在此基础上构建一个相当复杂的框架。Actor 可以在网络中交互,构成已知的架构,如分叉/合并结构、管道或共享工作队列。让我们来看一个分叉/合并的例子。

分叉/合并

分叉/合并求解器(5) 的简要总结如下:

Result solve(Problem problem)
{
  if (problem is small)
    directly solve problem
  else {
    split problem into independent parts
    fork new subtasks to solve each part
    join all subtasks
    compose result from subresults
  }
} 

假设我们有一些代码有两个可以并行执行的正交部分。使用 Actor 可以用简单的分叉/合并安排来相当简单地实现这一点。说明此问题的代码如下:

void foo()
{
  LongProcess1();
  LongProcess2();
}

void MyCode()
{
  foo();
}  

如果我们至少将其中一个实现为 Actor 对象,那么方法 foo() 可以重写为简单的分叉/合并,将两个部分用于一些加速。当然,为此,这两个函数需要彼此完全正交,没有共享数据以避免竞态条件。`LongProcess2()` 也必须是 void(或忽略返回值),因为它独立运行,我们无法从中获取返回值。

这看起来像: 

 typedef enum { DOTASK, STOPACTOR } MsgType_t;

// Message class
class Msg : public BaseMessage
{
public:
  Msg(int iValue)
  { m_iValue = iValue; }
  virtual ~Msg(){}

  int GetValue() const
  { return m_iValue; }

private:
  int m_iValue;
};


// Simple actor class to implement fork/join of a function
class MyActor : public HBActor
{
public:
  MyActor() {}
  virtual ~MyActor() {}
protected:
  virtual void Process(BaseMessage* pBMsg)
  {
    Msg*pMsg = (Msg*) pBMsg;
    if(STOPACTOR == pMsg->GetValue()) // Handle termination request
      Exit();
    else // Handle execution request
      LongProcess2();
    delete pBMsg;
  }
};

void foo()
{
  MyActor actor;
  actor.Start();
  actor.Send(new Msg(DOTASK));
  actor.Send(new Msg(STOPACTOR));
  LongProcess1();
  actor.Join();
}

void MyCode()
{
  foo();
}

这是分叉/合并模型的一个简单配置。请在 Google 上搜索一些更复杂的代码示例。

Actor 对象在其启动代码中激活一个线程,该线程可能会消耗资源并花费一些时间。线程的创建不是免费的或瞬时的。请确保 `LongProcess1()` 和 `LongProcess2()` 这两个 API 确实“长于”线程创建时间,否则您将在此实现中浪费时间。

示例代码中还包含另一个使用 Actor 管道计算素数列表的例子。

Actor 编程模型的局限性

为了完整起见,这里列出了一些 Actor 模型的事实:

  • 使用 Actor 减少了竞态条件的机制,但并未完全消除它们。如果 Actor 对象处理的消息或底层逻辑包含可变的共享对象,则可能发生数据竞态条件。实现真正并发的数据结构并非易事。Actor 模型在某些方面改进了这些问题,但并未解决所有问题。
  • 在许多情况下可能发生死锁。
  • Actor 模型实现了 Actor 方向的消息传递,但它并不支持发送请求并接收特定状态或对请求的回复。同步回复需要某种阻塞逻辑。有关可以提供此行为的对象的信息,请参阅“futures”。

脚注

  1. 来源:http://dl.acm.org/citation.cfm?id=1624804
  2. Actor 可以在计算机网络中执行,也可以作为不同地址空间中的多个进程执行。在本文中,我将 Actor 视为在单个机器上、同一地址空间中具有多个线程的 Actor。
  3. 还有一些关于“暂停”和“恢复”功能的工作,本文未予考虑。
  4. 一些参考资料并不保证此细节。为本文起见,将假定为该情况。
  5. 来源:http://gee.cs.oswego.edu/dl/papers/fj.pdf

关注点

我在网上看到过对 Actor 编程模型的各种评论,包括一些反对意见。我对该模型提供的功能非常满意。希望您也喜欢它!

在我编写代码的过程中,我曾将 Actor 模型用作日志类、缓冲 I/O 处理程序(输入和输出)以及迭代问题求解器。我非常喜欢它!

如上所述,此代码是在 Win32 上的 VS2010 开发的。我希望在其他编译器和其他平台上验证此代码。  如果您在其他平台上成功使用了它,请在下方留言。

历史  

  • 2012/10/20 初始版本。 
  • 2012/10/27 恢复丢失的项目符号,添加缺失的 Join() 调用,修复脚注引用。 

© . All rights reserved.