Chinaunix首页 | 论坛 | 博客
  • 博客访问: 1695331
  • 博文数量: 607
  • 博客积分: 10031
  • 博客等级: 上将
  • 技术积分: 6633
  • 用 户 组: 普通用户
  • 注册时间: 2006-03-30 17:41
文章分类

全部博文(607)

文章存档

2011年(2)

2010年(15)

2009年(58)

2008年(172)

2007年(211)

2006年(149)

我的朋友

分类: C/C++

2007-02-19 22:14:54

巧妙的C++同步
文/Truman Woo
概述
由于C++标准库不支持任何多线程编程实用工具,C++编程人员总是必须从头开始创建用于多线程编程的内容(例如线程和同步对象)。
此外,由于C++编程人员总是忙于从头开始创建同步对象,没办法太多地关注同步问题,从而导致处理多个线程的代码中出现较多的bug。本文将介绍几个多线程编程实用工具和惯例,希望
能对您的多线程编程工作有所帮助。

同步对象
大多数现代多任务操作系统都提供了自己的同步API和对象以便进行多线程编程。但是,作为C++编程人员,我们习惯于将所有内容看作对象,因此必须将这些功能封装到类中,并利用C++管理这些API 和对象。
有关提供这些封装类的内容超出了本文范围(从任何有关C++多线程编程的书中都可以找到相关详细实现),但是,本文还是会列出这些类的几个必要接口并进行进一步讨论。


锁用于防止多个线程同时访问一个共享对象,
因为这样可能会损坏共享对象的状态。
class Lock
{
public:
// 获取当前调用线程的锁。
bool Lockup() const;
// 释放当前调用线程的锁。
bool Unlock() const;
};
客户机调用Lock::Lockup()获取共享对象的锁并防止其他线程访问该对象,最后使用完该共享对象后它会调用Lock::Unlock()释放该锁。锁可在多个线程之间共享,一旦某个线程获得该锁,所有其他线程都必须在该锁处阻止,直到所有者释放该锁。

事件
顾名思义,事件用作线程通信的通知。
class Event
{
public:
// 等待在指定的时间内发出此事件。
bool Wait(unsigned long
milliseconds) const;
// 通知等待线程。
bool Notify() const;
};
一个客户机调用Event::Wait()等待某个事件的发生,该事件会阻止该客户机,直到另一个客户机调用Event::Notify()引发该事件的通知。通常情况下,等待着的客户机和进行通知的客户机处于不同的线程中,这也是它们需要使用此同步对象才能相互通信的原因。

C++ 实现Form
为了让讨论更加清晰和具体,让我们先从下面这个需要同步支持的例子说起:假设有个叫Form 的类,它含有一个储存其他对象传来消息的队列,并且它执行一个独立的进程处理未决消息。
假设多个线程可能会同时尝试将对象发送到表单中,所以我们必须序列化对表单的访问,尤其是序列化对内部事件队列的访问。因此,Form的典型C++实现应类似如下所示:
class Form : public Thread
{
public:
 // 发送指定信息到这个表单
 void PostMessage(const Message& message)
 {
[1] this->m_messageQueueLock.Lockup();
[2] this->m_messageQueue.PutMessage(message);
[3] this->m_messageQueueLock.Unlock();
[4] this->m_messageEvent.Notify();
 }
private:
 // 处理消息队列中的所有未决消息
 void ProcessMessages()
 {
[5] this->m_messageEvent.Wait(TIME_INFINITE);
[6] MessageQueue messagesToProcess;
[7] this->m_messageQueueLock.Lockup();
[8] this->m_messageQueue.Swap(messagesToProcess);
[9] this->m_messageQueueLock.Unlock();
[10] for (const Message* pMessage=
messagesToProcess.GetMessage();
[11] pMessage;
[12] pMessage = messagesToProcess.GetMessage())
[13] {
[14] this->OnMessage(*pMessage);
[15] messagesToProcess.PopMessage();
[16] }
 }
 // 进程入口
 virtual void OnRun()
 {
while (this->IsAlive())
{
this->ProcessMessages();
}
 }
 // 为子类处理消息
 virtual bool OnMessage (const Message& message) = 0;
private:
MessageQueue m_messageQueue;
Lock m_messageQueueLock;
Event m_messageEvent;
};
发现这个实现中的问题了吗?给您1 分钟的时间,看是否能找出2 个以上的问题..
好了,时间到。找出问题所在了吗?在此我列出两个问题:

1>>异常安全问题
严格说来,从异常安全的角度来说,PostMessage()和ProcessMessages()很容易出现问题,可能会永久阻止工作线程。
我们假定现在有一个表单正在处理队列中挂起的消息(第[10]-[15]行)。同时,另一个线程中的对象正在尝试将一条消息发送到消息队列中。它成功获得了队列的锁(第[1]行),但不幸的是PutMessage()因为某种原因抛出了一个异常(第[2]行),从而导致该工作线程直接关闭。由于所有者已经关闭,这个锁就没有机会得到释放,所有其他线程都将永久性地阻止在该锁上。这是多么恐怖的事情!
2>>维护问题
如您所见,为了同步对内部消息队列的多个访问,我们必须为其维护两个额外的同步对象(m_messageQueueLock 和m_messageEvent)。如果您有数十个对象要求进行同步,情况会怎样呢?这会是一个非常严重的维护问题,项目越大问题越严重。
借鉴Java 和C#的经验
与C++不同,Java和C#都在语言和开发包中内置了多线程支持和实用工具。现在我们看看这两种语言是如何处理多线程的,有哪些值得我们借鉴的经验(不要感到羞愧,Java和C#从C++借鉴了太多东西,现在我们只是小小地礼尚往来一下)。
鉴于本文介绍的是有关同步惯例的问题,我们将关注一下Java和C#是如何解决在上一节的结尾的问题的。
现在我们讨论一下Java。所有Java对象都从java.lang.Object派生而来,该对象提供了内置的同步功能,例如锁和事件。
Object 类提供的同步接口如下所示:
class Object {
public final void wait() throws InterruptedException;
public final void notify();
};
wait()和notify()方法提供的功能与前面讨论的Event类提供的功能相同。由于所有Java类均派生自Object,所以它们可以随时用作同步对象。非常方便,不是吗?
还有,派生自Object 的所有类(包括Object 本身)都具有一个不可见的内置锁,这个锁不能直接访问,但可通过下面的方式轻松地访问:
synchronized (this.myObject) {
this.myObject.doSomething1();
this.myObject.doSomething2();
this.myObject.doSomething3();
}
这在Java中称为同步块(C#也提供了类似机制)。进入同步块时,在同步对象上就可以自动获得内置锁;离开同步块时则会自动释放该锁。
此外,如果执行同步块时抛出了任何异常,也可以保证在异常传播出去之前自动释放这个不可见的锁。而且,如果某天您决定不再需要同步doSomething3(),只需上移最后的大括号即可从该块中删除doSomething3()。同样非常方便,不是吗?
好了,现在我们看看如何在Java中实现Form(根据Java编码风格,这里没有将每个方法名的首字母大写):
abstract class Form extends Thread {
 private MessageQueue messageQueue = new MessageQueue();
 public void postMessage(Message message) {
synchronized (this.messageQueue) {
  this.messageQueue.putMessage(message);
  this.messageQueue.notify();
}
}
 private void processMessages() {
MessageQueue messagesToProcess =
new MessageQueue();
synchronized (this.messageQueue) {
  this.messageQueue.wait();
  this.messageQueue.swap(messagesToProcess);
}
for (Message message =
    messagesToProcess.getMessage();
    message != null;
    message = messagesToProcess.getMessage()) {
  this.onMessage(message);
  messagesToProcess.popMessage();
}
 }
 void onRun() {
while (this.isAlive()) {
  this.processMessages();
}
 }
 abstract boolean onMessage (Message message);
};
干净简洁!这样我们就轻松获得了内置的异常安全保证。
一件多么有意义的事情!
但是等一等,作为C++编程人员,我们从这些同步机制可以学到什么?我们不具有任何C++ 语言支持(就某种程度而言,这并不正确);我们也不具有STL 库的任何支持。不过幸运的是,我们可以利用C++强大的可扩展性和灵活性来自行构建支持。

C++ 同步惯例
是的,C++是一种强大的面向对象的编程语言,但是不要忘记,它也是一种强大的泛型编程语言。不过,这与同步有什么关系呢?耐心一点,现在我们看一看可以从泛型编程中获得什么好处。

C++ 同步实用程序
首先,我列出所有我们会用来简化同步的实用程序。
template
struct SyncObject : public T
{
  typedef T ObjectType;
  T& GetObject()
  {
    return *this;
  };
  const T& GetObject() const
  {
    return *this;
  };
};
// 将锁功能注入模板参数。
template
struct Lockable : public SyncObject, public Lock
{
};
// 将事件功能注入模板参数。
template
struct Waitable : public SyncObject, public Event
{
};
#define SYNCHRONIZED
template
struct LockableUtil
{
  typedef T LockableType;
  typedef typename T::ObjectType ObjectType;
  explicit LockableUtil(T& lockable) : m_lockable(lockable)
  {
  }
  T& GetLockable()
  {
    return this->m_lockable;
  }
  const T& GetLockable() const
  {
    return this->m_lockable;
  }
  ObjectType& GetObject()
  {
    return this->m_lockable;
  }
  const ObjectType& GetObject() const
  {
    return this->m_lockable;
  }
private:
  T& m_lockable;
};
template
struct AutoLock : public LockableUtil
{
  explicit AutoLock(T& lockable) : LockableUtil(lockable)
  {
    this->GetLockable().Lockup();
  }
  ~AutoLock()
  {
    this->GetLockable().Unlock();
  }
};

C++ 同步惯例
现在我们使用这些实用工具重新实现Form 类,再看看可从中获得什么好处。
class Form : public Thread
{
private:
  typedef Lockable< Waitable >
SyncMessageQueue;
public:
  void PostMessage(const Message& message)
  {
    SYNCHRONIZED
    {
      AutoLock
lock(this->m_messageQueue);
      this->m_messageQueue.PutMessage(message);
    }
    this->m_messageQueue.Notify();
  }
private:
  void ProcessMessages()
  {
    this->m_messageQueue.Wait(TIME_INFINITE);
    MessageQueue messagesToProcess;
    SYNCHRONIZED
    {
      AutoLock
lock(this->m_messageQueue);
      this->m_messageQueue.Swap(messagesToProcess);
    }
    for (const Message* pMessage =
messagesToProcess.GetMessage();
pMessage;
pMessage = messagesToProcess.GetMessage())
    {
      this->OnMessage(*pMessage);
      messagesToProcess.PopMessge();
    }
  }
  virtual void OnRun()
  {
    while (this->IsAlive())
    {
      this->ProcessMessages();
    }
  }
virtual bool OnMessage (const Message& message) = 0;
private:
  SyncMessageQueue m_messageQueue;
};
比较前文的C+实现以及上节中的Java实现,就会发现上面列出的代码具有如下优势:
● 它具有与Java版本相同的可读性,同样干净,同样出色。
● 它可以无干扰地将事件和锁功能注入到普通对象(在本例中为MessageQueue)中,从而轻松而且中立地实现同步。
● 可在代码中自动构建异常安全。
首先我们看一下第2个优点,看看它是如何实现的。在Form类中,我们可以看到下面的定义。
typedef Lockable< Waitable >
SyncMessageQueue;
SyncMessageQueue m_messageQueue;
在第一行中,我们定义了一个新的同步类型,该类型将MessageQueue 和事件与锁功能集成在一起。在第二行中,我们定义了m_messageQueue 作为新的同步类型,因此我们可以对MessageQueue 应用下面的操作:
m_messageQueue.Lockup();
m_messageQueue.Unlock();
m_messageQueue.Wait(TIME_INFINITE);
m_messageQueue.Notify();
接下来我们看一下第3 个优点,请看下面的一段代码:
void PostMessage(const Message& message)
{
[1] SYNCHRONIZED
[2] {
[3] AutoLock
lock(this->m_messageQueue);
[4] this->m_messageQueue.PutMessage(message);
[5] }
[6]
[7] this->m_messageQueue.Notify();
}
第[1]行是一个宏,不起任何作用。它只是一个标记,用于增加代码可读性,并指出后面的块是重要部分。
第[2]行使用AutoLock来管理重要部分。注入了锁功能的消息队列自动锁定在AutoLock的构造函数中(第[5]行)。此外,无论是否抛出异常,该消息队列都会在析构函数中自动解锁。
第[4]行将消息推入到消息队列中。
第[7]行通知线程正在等待ProcessMessges()中的队列。
同样非常干净而简洁!

结束语
通过与Java和C#中的同步机制进行比较,我们发现手动管理C++同步对象难以避免异常,而且可能导致容易出现bug。
因此通过引入模板Lockable、Waitable和AutoLock,我们知道了如何将C++同步惯例应用于我们的代码,从而使得同步更容易、更简单、更漂亮,尤其是,它本身具有能够避免异常的特性。
最后,希望本文能够有助于您的日常编程工作,能够提供一个合适的实用程序而不必再经常从头开始创建它。感谢您花时间阅读!

Andrei Alexander 著
Modern C++ Design: Generic Programming and Design
Patterns Applied
David Vandevoorde, Nicolai M. Josuttis 著
C++ Templates: The Complete Guide

=============================================================================
下面给出我的类似实现
in autolock.h:

#ifndef _AUTOLOCK
#define _AUTOLOCK
#include
using namespace std;

// technology of returning self reference
template
class Reference: public T
{
public:
    T& GetObject()
    {
        cout<<"Reference::GetObject()"<
        return *this;
    };

    const T& GetObject() const
    {
        cout<<"Const Reference::GetObject()"<
        return *this;
    };


};

class Lock
{
public:
        Lock() { pthread_mutex_init(&m_lock,0); cout<<"init mutex"<
        ~Lock() { pthread_mutex_destroy(&m_lock); cout<<"destroy mutex"<

        bool Lockup() { pthread_mutex_lock(&m_lock); cout<<"lock mutex"<
        bool Unlock() { pthread_mutex_unlock(&m_lock);cout<<"unlock mutex"<
protected:
        pthread_mutex_t m_lock;
};

class WaitableLock: public Lock
{
public:
        WaitableLock(){ pthread_cond_init(&m_cond, 0);};
        ~WaitableLock(){ pthread_cond_destroy(&m_cond);};
        bool Wait(unsigned long to_seconds) {
            struct  timespec  timeout;
            timeout.tv_sec=time(NULL)+to_seconds;
            timeout.tv_nsec=0;
            pthread_cond_timedwait(&m_cond, &m_lock, &timeout);
            cout<<"wait for permission"<
        };
        bool Notify() {
            //pthread_cond_signal(&m_cond);
            pthread_cond_broadcast(&m_cond);
            cout<<"notify other waiters"<
        };
private:
        pthread_cond_t m_cond;
};

//The complete synchronized object
template
class SyncObject: public Reference, public WaitableLock
{

};

//AutoLock structure appear...
//L should be Lock or its derived
#define SYNCHRONIZED

template
class AutoLock
{
public:
    AutoLock(L& lockable):m_lockable(lockable){
        m_lockable.Lockup();
    };
    ~AutoLock() {
        m_lockable.Unlock();
    };
private:
    L& m_lockable;

};

#endif


in test.cpp:
#include
#include "autolock.h"
using namespace std;

class XClass{
public:
    XClass(){cout<<"XClass"<
    ~XClass(){cout<<"~XClass"<
    void set(){ m_status=1; cout<<"handle XClass object: set"<
    void reset(){m_status=0;cout<<"handle XClass object: reset"<
private:
    int m_status;
};


int main(int argc, char** argv)
{

    typedef SyncObject SyncX;

    SyncX so;

    so.Wait(5);
    SYNCHRONIZED
    {
        AutoLock al(so);
        // TODO: do something for so
        so.GetObject().set();
        so.GetObject().reset();

        XClass& xo= (XClass&)so.GetObject();
        xo.set();

    }
    so.Notify();

    return 0;
}



阅读(1261) | 评论(0) | 转发(0) |
给主人留下些什么吧!~~