5
\$\begingroup\$

I tried to create a class for porting Java's synchronized keyword to C++ using below code using *nix pthread's library.

In general my test cases seem to work, but since this is a very critical topic I would like to have someone else have a look on it in case I missed something which might invalidate my test cases.

The basic idea is that I created a synchronized(){} block using a #define with help of an if() statement (at the very end of the source).

The synchronized(variableORpointer){} block uses a variable as a reference OR a pointer and using template functions it finally creates/deletes a mutex and condition variable dynamically for the given address using a pointer to referent OR given pointer.

It also supports Java's notify/notifyAll/wait functions using Synchronized::notify(variable), Synchronized::notifyAll(variable) and Synchronized::wait(variable, timeoutInMs, nanos) functions.

A null pointer or not holding the lock on calling wait() causes an std::runtime_error to be thrown.

The most current code is hosted on GitHub including a test example.

#ifndef __SYNCHRONIZED_HPP__
#define __SYNCHRONIZED_HPP__

#include <pthread.h>
#include <map>
#include <iostream>
#include <stdexcept>
#include <typeinfo>
#include <sys/time.h>
#include <errno.h>

class Synchronized{
protected:
    typedef struct metaMutex{
        pthread_mutex_t lock;
        pthread_cond_t cond;

        pthread_t lockOwner;

        int counter;
    } metaMutex;

    pthread_mutex_t& getMutex(){
        static pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER;
        return lock;
    }
    std::map<const void*, metaMutex*>& getMutexMap(){
        static std::map<const void*, metaMutex*> mmap;
        return mmap;
    }

    const void *accessPtr;
    metaMutex *metaPtr;
    int dereference;

    template<typename T>
    T * getAccessPtr(T & obj) { return &obj; } //turn reference into pointer!
    template<typename T>
    T * getAccessPtr(T * obj) { return obj; } //obj is already pointer, return it!

public:
    template<typename T>
    Synchronized(const T &ptr, bool lockit=true) : accessPtr(getAccessPtr(ptr)){
        //std::cout << "type: " << typeid(ptr).name() << std::endl;

        if(this->accessPtr==NULL){
            throw std::runtime_error(std::string("Synchronizing on NULL pointer is not valid, referenced type is: ")+typeid(ptr).name());
        }

        pthread_mutex_lock(&this->getMutex());

        std::map<const void*, metaMutex*>& mmap = this->getMutexMap();
        std::map<const void*, metaMutex*>::iterator it = mmap.find(this->accessPtr);
        if(it != mmap.end()){
            this->metaPtr = it->second;
            this->metaPtr->counter++;
        }
        else{
            this->metaPtr = new metaMutex();
            pthread_mutex_init(&this->metaPtr->lock, NULL);
            pthread_cond_init(&this->metaPtr->cond, NULL);
            this->metaPtr->counter = 1;
            mmap.insert(std::make_pair(this->accessPtr, this->metaPtr));
        }

        pthread_mutex_unlock(&this->getMutex());

        if(lockit){
            pthread_mutex_lock(&this->metaPtr->lock);
            this->metaPtr->lockOwner = pthread_self();
        }
    }

    operator int() { return 1; }
    const void* getSynchronizedAddress(){
        return this->accessPtr;
    }

    ~Synchronized(){
        pthread_mutex_unlock(&this->metaPtr->lock);

        pthread_mutex_lock(&this->getMutex());
        metaPtr->counter--;
        if(metaPtr->counter<=0){
            this->getMutexMap().erase(this->accessPtr);
            delete metaPtr;
        }
        pthread_mutex_unlock(&this->getMutex());
    }

    void wait(unsigned long milliseconds=0, unsigned int nanos=0){
        if(pthread_equal(pthread_self(), this->metaPtr->lockOwner)==0){
            throw std::runtime_error(std::string("trying to wait is only allowed in the same thread holding the mutex"));
        }

        int rval = 0;
        if(milliseconds == 0 && nanos == 0){
            rval = pthread_cond_wait(&this->metaPtr->cond, &this->metaPtr->lock);
        }
        else{
            struct timespec timeUntilToWait;
            struct timeval now;
            int rt;

            gettimeofday(&now,NULL);

            timeUntilToWait.tv_sec = now.tv_sec;
            long seconds = 0;
            if(milliseconds >= 1000){
                seconds = (milliseconds/1000);
                milliseconds -= seconds*1000;
            }
            timeUntilToWait.tv_sec += seconds;
            timeUntilToWait.tv_nsec = (now.tv_usec+1000UL*milliseconds)*1000UL + nanos;
            rval = pthread_cond_timedwait(&this->metaPtr->cond, &this->metaPtr->lock, &timeUntilToWait);
        }
        switch(rval){
            case 0:
                this->metaPtr->lockOwner = pthread_self();
                break;
            case EINVAL: throw std::runtime_error("invalid time or condition or mutex given");
            case EPERM: throw std::runtime_error("trying to wait is only allowed in the same thread holding the mutex");
        }
    }
    void notify(){
        if(pthread_cond_signal(&this->metaPtr->cond)!=0){
            std::runtime_error("non initialized condition variable");
        }
    }
    void notifyAll(){
        if(pthread_cond_broadcast(&this->metaPtr->cond)!=0){
            std::runtime_error("non initialized condition variable");
        }
    }

    template<typename T>
    static void wait(const T &ptr, unsigned long milliseconds=0, unsigned int nanos=0){
        Synchronized syncToken(ptr, false);
        syncToken.wait(milliseconds, nanos);
    }
    template<typename T>
    static void notify(const T &ptr){
        Synchronized syncToken(ptr, false);
        syncToken.notify();
    }
    template<typename T>
    static void notifyAll(const T &ptr){
        Synchronized syncToken(ptr, false);
        syncToken.notifyAll();
    }
};

/* from http://stackoverflow.com/questions/1597007/creating-c-macro-with-and-line-token-concatenation-with-positioning-macr */
#define synchronizedTokenPaste(x,y) x ## y
#define synchronizedTokenPaste2(x,y) synchronizedTokenPaste(x,y)
#define synchronized(ptr) if(Synchronized synchronizedTokenPaste2(sync_, __LINE__) = Synchronized(ptr))
#endif
\$\endgroup\$
  • 2
    \$\begingroup\$ Is C++11 an option? \$\endgroup\$ – Barry Sep 26 '15 at 16:19
  • \$\begingroup\$ Careful with names starting with a double underscore. You see a lot of those in the standard library precisely because they are reserved for library use: stackoverflow.com/q/228783/1198654 \$\endgroup\$ – glampert Sep 26 '15 at 18:09
  • \$\begingroup\$ @Barry in general yes, cause i implemented it this way out of interest and with the best of my (non existing C++11) knowledge - additionally it would be very kind if you might have a look at my code too regarding possible synchronization flaws in my code since i'm not that much expirienced with condition variables and i'm also a bit unsure about the last lines in the constructor on unlocking/locking might cause a race condition? \$\endgroup\$ – John Doe Sep 27 '15 at 10:31
  • \$\begingroup\$ @glampert you mean the define symbol? Thanks for your hint - such detailed standard naming conventions are pretty new to me \$\endgroup\$ – John Doe Sep 27 '15 at 10:33
4
\$\begingroup\$

C++ is not Java

I cannot stress that enough. You're trying to shoehorn a Java thing into C++, and we end up with macros. Macros are an awful solution to just about every problem and unless you have a compelling reason to use one, don't. This is not a compelling reason. For instance, we could just write:

synchronized(container, [&]{
    // stuff
});

It's a couple more characters than your solution, while also not having to be a macro. I think that's worth it.

Also, let's use some C++11 features, which are way easier to use with the introductions of <mutex> and <condition_variable> and <unordered_map>.

synchronized(), as a function

If you wrote it as a function, it would look like:

template <typename T, typename F>
void synchronized(T const& obj, F&& func)
{
    std::lock_guard<std::mutex> outer(SynchroObjects::synchro_mutex);
    std::lock_guard<std::mutex> inner(_synchro_objects[&obj].mutex);
    std::forward<F>(func)();
}

That's it. Sure, I have to show you what _synchro_objects is. But this is the power of C++11. We use RAII for locking (no lock() or unlock()), keep the mutex in a map, and don't need macros. That is super cool.

We just need some other utilities:

struct SynchroObjects {
    static std::mutex synchro_mutex;
    std::mutex mutex;
    std::condition_variable cv;
};

static std::unordered_map<const void*, SynchroObjects> _synchro_objects;

Other utility functions

Once we approach things from this perspective, all the other functions just sort of fall into place and are all just a couple lines:

template <typename T>
SynchroObjects& getSynchro(T const& container) {
    std::lock_guard<std::mutex> lk(SynchroObjects::synchro_mutex);
    return _synchro_objects[&container];
}

template <typename T>
void notify(T const& container) {
    getSynchro(container).cv.notify_one();
}

template <typename T>
void wait(T const& container) {
    // potentially you could add some safety checking to ensure 
    // that _synchro_objects[&container] exists here, maybe use
    // at() in debug mode or something

    auto& sync = getSynchro(container);
    std::unique_lock<std::mutex> lk(sync.mutex);
    sync.cv.wait(lk);
}

// wait(T const&, Predicate);
// wait_for(...);
// wait_until(...);
\$\endgroup\$
  • \$\begingroup\$ Thats a really awesome example of using many new c++11 features i'm not yet much aware of . Thanks for sharing your enthusiasm on c++11. Regarding your code, am i right that there is a flaw in the synchronized function cause of not locking _synchro_objects itself? So this is meant as c++11-focused example code, right? Could you also elaborate a bit on your first paragraph? Cause the java design pattern on hiding mutex's and focusing on the resource for synchronization is pretty valuable. Thats why i think synchronizing via the resource itself would be great in c++ and/or c++11. \$\endgroup\$ – John Doe Sep 27 '15 at 10:37
  • \$\begingroup\$ @JohnDoe Good point, I corrected the flaw. Also, the Java design pattern is valuable but C++ is not Java. In C++, we do C++ things. If you need a specific container synchronized, you should just have a mutex along with that container somewhere - don't add this extra outer layer to it. \$\endgroup\$ – Barry Sep 27 '15 at 10:46
  • \$\begingroup\$ Thanks - I'll try to merge this all in some kind of {Synchronized localVar(resource); } way \$\endgroup\$ – John Doe Sep 27 '15 at 10:49
  • \$\begingroup\$ «Macros are an awful solution to just about every problem»: I dare to vehemently disagree. The only problem with macros is that they have global scope, but then again so do most of the language keywords anyway: you cannot use "const" as the name of a variable, for instance, and that's no different than declaring that synchronized is a new keyword injected in the language by an external library of sorts. The preprocessor is part of the language, after all. \$\endgroup\$ – Fabio A. Jun 21 '18 at 8:34

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.