C++ Synchronization Idioms

 

C++ Synchronization Idioms

TrumanWoo@hotmail.com

1. Overview

Since C++ standard library doesn’t provide any multi-thread programming utility, C++ programmer always has to reinvent the wheel to create the necessities for multi-thread programming, such as thread and synchronization objects.

 

Moreover, since C++ programmer is always busy in reinventing the wheel, he is not able to pay enough attention to synchronization issues, which may raise bugs in the code dealing with multiple threads. In this paper, I will present several multi-thread programming utilities and idioms, and hope that they can help you in your multi-thread programming.

 

The paper will cover the following contents:

1.       An example that requires synchronization.

2.       Synchronization objects.

3.       Synchronization mechanisms in Java and C#.

4.       Synchronization utilities and idioms in C++.

2. An example for discussion

In order to make our discussion more concrete and clear, let’s begin with the following example that require synchronization support: suppose we have a class named Form, which maintains an internal queue to store messages posted by other objects, and it executes in a separate thread to process pending messages. So we have the following classes to deal with:

 

class Message

{

    virtual ~Message() { }

    ...

};

 

class MessageQueue

{

public:

    // Push the specified message into the end of this queue.

    void PutMessage (const Message& message);

 

    // Remove the message in the head of this queue.

    void PopMessage();

 

    // Access the message in the head of this queue.

    const Message* GetMessage() const;

 

    // Swap messages in this queue for those in the source queue.

    void Swap(MessageQueue& source);

 

    ...

};

 

class Thread

{

public:

    // Check whether this thread is still alive or not.

    bool IsAlive();

 

private:

    // Thread entrance point.

    virtual void OnRun() = 0;

 

    ...

};

 

static unsigned long TIME_INFINITE = (unsigned long)(-1);

 

class Form : public Thread

{

public:

    // Post the specified message to this form.

    void PostMessage(const Message& message);

 

private:

    // Process all the pending messages in the message queue.

    void ProcessMessages();

   

    virtual void OnRun() { this->ProcessMessages(); }

 

    // For subclass to process message.

    virtual bool OnMessage (const Message& message) = 0;

 

private:

    MessageQueue m_messageQueue;

};

3. Synchronization objects

Most modern multi-task operation systems provide their synchronization APIs and objects for multi-thread programming. However, as a C++ programmer, we get used to treating everything as object, so we have to wrap these functionalities into classes, and take the advantages of C++ to manage those APIs and objects.

 

Providing these wrapper classes is beyond the scope of this paper (you can pick up any book about C++ multi-thread programming, and refer to the implementations for details), however, I will list the minimal necessary interfaces of these classes for further discussion.

 

3.1 Lock

Lock is used to prevent multiple threads from accessing a shared object at the same time, which may corrupt the state of the shared object.

 

class Lock 

{

public:

    // Acquire this lock for current calling thread.

    bool Lockup() const;

 

    // Release this lock for current calling thread.

    bool Unlock() const;

};

 

Client calls Lock::Lockup() to acquire a lock for a shared object and prevents other threads from accessing the object, and finally it calls Lock::Unlock() to release the lock after finishes using the shared object. A lock can be shared among multiple threads, once a thread acquire the lock, all the other threads have to be blocked on this lock until the owner releases it.

 

3.2 Event

As its name implies, event is used as notification for thread communication.

 

class Event 

{

public:

    // Wait for this event to be signaled in the specified time.

    bool Wait(unsigned long milliseconds) const;

 

    // Notify the waiting threads.

    bool Notify() const;

};

 

One client calls Event::Wait() to wait for a certain event to occur, which blocks the client until another client calls Event::Notify() to fire the notification for that event. Typically, the waiting client and notifying client are in different threads, that’s why they need this synchronization object to communicate with each other.

4. Implementation of Form in C++

Since multiple threads may try to post events into a form at the same time, we have to serialize the accesses to the form, more specifically, to serialize the accesses to the internal event queue. So a typical C++ implementation of Form might look like follows:

 

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]     this->m_messageQueueLock.Lockup();

[7]     MessageQueue messageToProcess;

[8]     this->m_messageQueue.Swap(messageToProcess);

[9]     this->m_messageQueueLock.Unlock();

       

[10]    for (const Message* pMessage = messageToProcess.GetMessage();

[11]       pMessage;

[12]       pMessage = messageToProcess.GetMessage())

[13]    {

[14]        this->OnMessage(*pMessage);

[15]    }

}

   

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;

};

 

Can you find any issues in this implementation? Give you 1 minute to find out at leave 2 issues...

 

OK, time is up. Have you found any issues? I have two for you:

 

Exception safe issue:

Strictly speaking, in the respective of exception safe, PostMessage() and ProcessMessages() are bug-prone and may cause the worker thread to be blocked forever. Let’s say, a form is now processing pending messages in the queue (Line [10]-[15]). At the same time, an object in another thread is trying to post a message into the message queue. It succeeds in acquiring the lock for the queue (Line [1]), but unfortunately, PutMessage() throws an exception for some reason (Line [2]), which kills the worker thread immediately. Because the owner has died, the lock will never have a chance to be released, and all the other threads will be blocked on it forever. How terrible it is! 

 

Maintaining issue:

As you can see, in order to synchronize multiple accesses to the internal message queue, we have to maintain two extra synchronization objects (m_messageQueueLock and m_messageEvent) for it. How about if we have dozens of objects that require synchronization? That should be a serious maintaining issue, and it will be even worse for large projects.

5. What can we learn from Java and C#

Unlike C++, both Java and C# build multi-thread supporting and utilities into the languages and the development kits. Let’s take a look at how these languages deal with multi-thread, and “borrow” everything we can from them (don’t feel ashamed about it, Java and C# have borrowed too much things from C++, and we just get back part of those they should return).

 

Since this paper is about synchronization idioms, we will focus on how Java and C# solve the issues in the end of section 4.

 

Let’s take Java for discussion. All Java objects are derived from java.lang.Object, which provides build-in synchronization functionalities such as lock and event. The synchronization interfaces provided by class Object is listed as follows:

 

class Object {

    public final void wait() throws InterruptedException;

    public final void notify();

};

 

The methods wait() and ntofiy() provide the same functions as those in class Event we described earlier. Since all Java classes are derived from Object, they can be used as synchronization objects for free. Neat and elegant, isn’t it?

 

Moreover, every class derived from Object (including Object itself) has an invisible build-in lock, which cannot be accessed directly but can be used easily as follows:

 

synchronized (this.myObject) {

    this.myObject.DoSomething1();

    this.myObject.DoSomething2();

    this.myObject.DoSomething3();

}

 

It’s called synchronization block in Java (C# also provides a similar mechanism). When entering synchronization block, the build-in lock is acquired automatically on the synchronized object; when leaving synchronization block, the lock is released automatically.

 

Further more, if any exception is thrown during executing the synchronization block, the invisible lock is guaranteed to be release automatically before the exception propagates out. And if one day you decided that there’s no need for DoSomething3() to be synchronized, just move up the right bracket and make DoSomething3() out of the block. Also neat and elegant, isn’t it?

 

OK, now let’s take a look at how to implement Form in Java (According to Java coding style, I don’t capitalize the first letter for each method name):

 

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() {

        this.messageQueue.wait();

 

        MessageQueue messageToProcess = new MessageQueue();

        synchronized (this.messageQueue) {       

            this.messageQueue.swap(messageToProcess);

        }

       

        for (Message message = messageToProcess.getMessage();

            message != null;

            message = messageToProcess.getMessage()) {

            this.onMessage(message);

        }

    }

   

    void OnRun() {

        while (this.isAlive()) {

            this.processMessages();

        }

    }

 

    abstract boolean onMessage (Message message);

};

 

Neat and elegant! And we have the build-in exception safe guarantee for free. How wonderful it is!

 

But wait, as C++ programmers, what can we learn from those synchronization mechanisms? We don’t have any support from C++ language (to some extent, that’s not true); we don’t have any support from STL library. But fortunately, we can take the wonderful extensibility and flexibility of C++, to build the support ourselves.

6. The C++ synchronization idioms

Yes, C++ is a powerful language for object-oriented programming, but don’t forget, it is a powerful generic programming language too. But wait, what’s the business with synchronization? Keep patient, let’s have a look at what we can benefit from generic programming.

 

4.1 C++ synchronization utilities

 

template<typename T>

struct SyncObject : public T

{

    typedef T ObjectType;

 

    T& GetObject()

    {

        return *this;

    };

 

    const T& GetObject() const

    {

        return *this;

    };

};

 

// Inject lock functionalities into the template parameter.

template<typename T>

struct Lockable : public SyncObject<T>, public Lock

{

   

};

 

// inject event functionalities into the template parameter.

template<typename T>

struct Waitable : public SyncObject<T>, public Event

{

 

};

 

#define SYNCHRONIZED

 

template<typename T>

struct LockableUtil

{

    typedef T LockableType;

    typedef typename T::ObjectType ObjectType;

 

    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<typename T>

struct AutoLock : public LockableUtil<T>

{

    explicit AutoLock(T& lockable) : LockableUtil<T>(lockable)

    {

        this->GetLockable().Lockup();

    }

 

    ~AutoLock()

    {

        this->GetLockable().Unlock();

    }

};

 

4.2 C++ synchronization idioms

 

Now let’s re-implement the Form class with these utilities, and see the benefits we gain from them.

 

class Form : public Thread

{

private:

    typedef Lockable< Waitable<MessageQueue> > SyncMessageQueue;

 

public:

    void PostMessage(const Message& message)

    {

        SYNCHRONIZED

        {

            AutoLock<SyncMessageQueue> lock(this->m_messageQueue);

            this->m_messageQueue.PutMessage(message);

        }

 

        this->m_messageQueue.Notify();

    }

 

private:

    void ProcessMessages()

    {

        this->m_messageQueue.Wait(TIME_INFINITE);

 

        MessageQueue messageToProcess;

        SYNCHRONIZED

        {

            AutoLock<SyncMessageQueue> lock(this->m_messageQueue);

            this->m_messageQueue.Swap(messageToProcess);

        }

       

        for (const Message* pMessage = messageToProcess.GetMessage();

            pMessage;

            pMessage = messageToProcess.GetMessage())

        {

            this->OnMessage(*pMessage);

        }

    }

   

    virtual void OnRun()

    {

        while (this->IsAlive())

        {

            this->ProcessMessages();

        }

    }

 

    virtual bool OnMessage (const Message& message) = 0;

 

private:

    SyncMessageQueue m_messageQueue;

};

 

Compared with the first C++ implementation in section 4 and the Java implementation in section 5, we can see that the code listed above has the following advantages:

1.       It is nearly as readable ,neat and elegant as the Java version.

2.       It non-intrusively inject event and lock functionalities into plain object (MessageQueue in this example), and make synchronization easy and natural.

3.       It automatically builds exception safe into the code.

 

First, let’s take a look at advantage 2 and see how it is achieved. In class Form, we can see the following definitions.

 

typedef Lockable< Waitable<MessageQueue> > SyncMessageQueue;

SyncMessageQueue m_messageQueue;

 

In the first line, we define a new synchronization type, which integrates MessageQueue with the functionalities of even and lock. In the second line, we define m_messageQueue as the new synchronization type, so that we can apply the following operations on MessageQueue:

 

m_messageQueue.Lockup();

m_messageQueue.Unlock();

m_messageQueue.Wait(TIME_INFINITE);

m_messageQueue.Notify();

 

Then, let’s take a look at advantage 3 and take the following piece of code into account:

 

void PostMessage(const Message& message)

{

[1] SYNCHRONIZED

[2] {

[3]     AutoLock<SyncMessageQueue> lock(this->m_messageQueue);

[4]     this->m_messageQueue.PutMessage(message);

[5] }

[6]

[7] this->m_messageQueue.Notify();

}

 

Line [1] is a macro and actually does nothing at all. It is just a mark, which increases code readability, and indicates that the following block is a critical section.

 

Line [2] uses an AutoLock to manage the critical section. The message queue, which is injected with lock functionalities, is locked automatically in AutoLock’s constructor (Line [5]). Moreover, no matter exceptions occur or not, the message queue is guaranteed to be unlocked automatically in its destructor.

 

Line [4] pushes the message into the message queue.

 

Line [7] notifies the thread waiting on this queue in ProcessMessges().

 

Also neat and elegant, isn’t it!

7. Summary

Through comparison with the synchronization mechanisms in Java and C#, we know that manually managing C++ synchronization object is not exception-safe and might be bug-prone. And then, by introduction the templates Lockable<T>, Waitable<T> and AutoLock<T>, we know how to apply the C++ synchronization idioms to our code, which will make synchronization easier, simpler, more elegant, and more importantly, it is naturally exception-safe.

 

Finally, I hope this paper can help you in everyday programming and provide you with row wheels so that you don’t need to reinvent them again and again. Thanks so much for sparing your time!

 

8. Reference

Andrei Alexandrescu

Modern C++ Design

Generic Programming and Design Patterns Applied

Addison-Wesley, Reading , MA , 2001

 

David Vandevoorde, Nicolai M. Josuttis

C++ Templates: The Complete Guide

Addison-Wesley, November 12, 2002

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值