WEB开发网
开发学院软件开发VC Visual C++优化对大型数据集合的并发访问 阅读

Visual C++优化对大型数据集合的并发访问

 2010-08-22 20:47:32 来源:WEB开发网   
核心提示:WorkerThread 类WorkerThread 类(参见图 9 和图 10)显然派生自 Thread,构造函数获得一串输入参数,Visual C++优化对大型数据集合的并发访问(7),并且只是将其存储到数据成员中以便供以后使用,run 方法使用其中前三个参数来控制循环,然后,另一个正在等待读取该对象的线程将获取驻

WorkerThread 类

WorkerThread 类(参见图 9 和图 10)显然派生自 Thread。构造函数获得一串输入参数,并且只是将其存储到数据成员中以便供以后使用。run 方法使用其中前三个参数来控制循环。这在开始时可能看起来有点奇怪,但其意义在于尽可能均匀地将迭代总次数分配到所有线程中,即使迭代次数并不能由线程个数整除。如果发生这种情况,则某些线程将比其他线程多迭代一次。

在每次迭代中,run 方法都会调用 getRequest 以获取下一个请求的参数。在实际程序中,getRequest 将通过某种网络传输机制来从客户端获取输入。为使讨论简单化,我编写了 getRequest 以生成随机请求。为了防止这一方法使结果失真,我不得不使用我自己的随机数生成器。这是因为编译器的随机数生成器使用全局种子,因此需要一个临界区。这会引入额外的线程同步,从而掩盖我试图观察的结果。我的随机数生成器使用线程本地种子(存储在 WorkerThread 类中),因此不需要同步。

根据请求类型的不同,run 方法随后会调用 doUpdate 或 doRead。这些方法中的每个方法都会获取一个对所指示的数据库项的引用,锁定该引用,执行请求的操作,然后返回。请注意,使用了 LockKeeper 对象来直接锁定数据库项。这是一个示例,说明了如何使用 LockKeeper 模板来锁定任何具有不带任何参数的 acquire 和 release 方法的对象。稍后,我将详细讨论 DBEntry 类上的 acquire 和 release 方法。

还请注意,doUpdate 和 doRead 方法调用了 simulateWork 方法。在实际的服务器中,更新和读取(尤其是更新)会引起一些开销。为了使 doUpdate 和 doRead 占用足够的执行时间来显示期望的结果,我需要添加对 simulateWork 的调用。在我用作基准测试的计算机上,这会使 doRead 和 doUpdate 的执行时间分别增加大约 20 和 200 微秒。(这里需要称赞一下 Visual C++ 优化器:我必须向 simulateWork 添加一个全局访问,以防止该方法以及对它的调用被完全优化掉。)

在 doRead 和 doUpdate 中有一个难以发现的微妙之处。如果您大体考察一下 CD 代码,您会发现每当将字符串作为 in 参数传递给方法时,该参数的类型都是 const 字符串引用,而每当从方法中返回字符串时,返回类型都是字符串。然而,DBEntry 上的 setValue 和 getValue 方法却获取并返回 const char*。您可能认为这只是我在 C++ 标准库出现之前的多年 C 和 C++ 编码经验所遗留下来的习惯,但这完全是有意的。两个字符串对象可以包含对同一字符串数组的引用,以便优化内存使用率,减少复制字符串所花的时间。如果 setValue 获取 const 字符串引用并将其赋给 m_value 成员,则该成员将仅包含对传入的字符串的引用。这可能导致两个线程同时访问同一字符串数组。因为字符串类未经过同步,所以这可能导致堆损坏。我的解决方案是改为传递(或返回)const char*。这会在锁被 DBEntry 对象占有时强制进行字符串复制。进行复制之后,其他线程就可以接触 DBEntry 而不会造成损害。

问题和可能的解决方案

既然您已基本了解该示例程序所做的工作,那么让我们考察一下下面的问题:如何同步对数据库项的访问?您不能允许辅助线程杂乱无章地获取和设置数据库项,因为产生的竞争情形在最好的情况下会导致不正确的结果,在最坏的情况下会导致服务器崩溃。

最简单且最明显的解决方案是使用单个临界区。任何要读取或更新某个项的线程都必须首先获取临界区。在许多情况下,这是最适当的解决方案。然而,请注意它会过分地同步线程。两个正在访问不同项的线程根本无须进行同步,但使用单个临界区却无论如何都会将它们同步。如果多个线程经常需要同时访问不同的项,则这将在您的服务器中导致瓶颈,并且限制它的并发性。

另一种解决方案是在 DBEntry 本身内部嵌入一个临界区。该解决方案具有两个讨厌的问题。首先,它会消耗数量惊人的系统资源。每个临界区都使用 24 字节的内存以及一个事件内核对象。因此,如果您的数据库中含有 1,000,000 个项,则该方法将消耗 24MB 内存和 1,000,000 个事件,而这一切仅仅是为了进行同步!这将不会受到您的客户的欢迎。

该方法的另一个问题是您无法使用它来同步项的删除。(这在该示例中不是一个问题,但在实际情况中通常是一个问题。)要删除某个项,您需要锁定它,然后删除它。然后,另一个正在等待读取该对象的线程将获取驻留在已删除的内存中的临界区。这一点绝对不会受到客户的欢迎。

Figure 11 CritSecTable Class Definition

class CritSecTable
{
public:
  CritSecTable()
    {}
  ~CritSecTable()
    {}
  void acquire(void* p)
    { m_table[hash(p)].acquire(); }
  bool tryAcquire(void* p)  // use only on Windows NT or Windows 2000
    { return m_table[hash(p)].tryAcquire(); }
  void release(void* p)
    { m_table[hash(p)].release(); }
private:
  static unsigned char hash(void* p);
  CritSec m_table[256];
};

Figure 12 CritSecTable Class Implementation

unsigned char CritSecTable::hash(void* p)
{
  unsigned char result;
  if (sizeof(void*) == 8)
  {
    assert(sizeof(unsigned long) == 4);
    assert(sizeof(unsigned short) == 2);
    unsigned long temp1 = ((unsigned long*) (&p))[0]
      ^ ((unsigned long*) (&p))[1];
    unsigned short temp2 = ((unsigned short*) (&temp1))[0]
      ^ ((unsigned short*) (&temp1))[1];
    result = ((unsigned char*) (&temp2))[0]
      ^ ((unsigned char*) (&temp2))[1];
  }
  else if (sizeof(void*) == 4)
  {
    assert(sizeof(unsigned short) == 2);
    unsigned short temp = ((unsigned short*) (&p))[0]
      ^ ((unsigned short*) (&p))[1];
    result = ((unsigned char*) (&temp))[0]
      ^ ((unsigned char*) (&temp))[1];
  }
  else
  {
    result = ((unsigned char*) (&p))[0];
    for (unsigned i = 1; i < sizeof(void*); i++)
    {
      result ^= ((unsigned char*) (&p))[i];
    }
  }
  return result;
}

上一页  2 3 4 5 6 7 8 9  下一页

Tags:Visual 优化 大型

编辑录入:爽爽 [复制链接] [打 印]
赞助商链接