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
-
2\$\begingroup\$ Is C++11 an option? \$\endgroup\$Barry– Barry2015年09月26日 16:19:51 +00:00Commented Sep 26, 2015 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– glampert2015年09月26日 18:09:24 +00:00Commented Sep 26, 2015 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– John Doe2015年09月27日 10:31:46 +00:00Commented Sep 27, 2015 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– John Doe2015年09月27日 10:33:06 +00:00Commented Sep 27, 2015 at 10:33
1 Answer 1
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(...);
-
\$\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– John Doe2015年09月27日 10:37:52 +00:00Commented Sep 27, 2015 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– Barry2015年09月27日 10:46:04 +00:00Commented Sep 27, 2015 at 10:46 -
\$\begingroup\$ Thanks - I'll try to merge this all in some kind of {Synchronized localVar(resource); } way \$\endgroup\$John Doe– John Doe2015年09月27日 10:49:46 +00:00Commented Sep 27, 2015 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.– Fabio A.2018年06月21日 08:34:06 +00:00Commented Jun 21, 2018 at 8:34