巧妙的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;
}