views:

93

answers:

2

I have a class called Root which serves as some kind of phonebook for dynamic method calls: it holds a dictionary of url keys pointing to objects. When a command wants to execute a given method it calls a Root instance with an url and some parameter:

root_->call("/some/url", ...);

Actually, the call method in Root looks close to this:

// Version 0
const Value call(const Url &url, const Value &val) {
  // A. find object
  if (!objects_.get(url.path(), &target))
    return ErrorValue(NOT_FOUND_ERROR, url.path());
  }

  // B. trigger the object's method
  return target->trigger(val);
}

From the code above, you can see that this "call" method is not thread safe: the "target" object could be deleted between A and B and we have no guarantee that the "objects_" member (dictionary) is not altered while we read it.

The first solution that occurred to me was:

// Version I
const Value call(const Url &url, const Value &val) {
  // Lock Root object with a mutex
  ScopedLock lock(mutex_);

  // A. find object
  if (!objects_.get(url.path(), &target))
    return ErrorValue(NOT_FOUND_ERROR, url.path());
  }

  // B. trigger the object's method
  return target->trigger(val);
}

This is fine until "target->trigger(val)" is a method that needs to alter Root, either by changing an object's url or by inserting new objects. Modifying the scope and using a RW mutex can help (there are far more reads than writes on Root):

// Version II
const Value call(const Url &url, const Value &val) {
  // A. find object
  {
    // Use a RW lock with smaller scope
    ScopedRead lock(mutex_);
    if (!objects_.get(url.path(), &target))
      return ErrorValue(NOT_FOUND_ERROR, url.path());
    }
  }
  // ? What happens to 'target' here ?

  // B. trigger the object's method
  return target->trigger(val);
}

What happens to 'target' ? How do we ensure it won't be deleted between finding and calling ?

Some ideas: object deletion could be post-poned in a message queue in Root. But then we would need another RW mutex read-locking deletion on the full method scope and use a separate thread to process the delete queue.

All this seems very convoluted to me and I'm not sure if concurrent design has to look like this or I just don't have the right ideas.

PS: the code is part of an open source project called oscit (OpenSoundControl it).

+2  A: 

To avoid the deletion of 'target', I had to write a thread safe reference counted smart pointer. It is not that hard to do. The only thing you need to ensure is that the reference count is accessed within a critical section. See this post for more information.

Dimitri C.
That's a good idea. I just have to take care of the --ref_count and unregister order: unregister first (the read lock would prevent this), then --ref_count (increased ref_count would prevent deletion).
Gaspard Bucher
If you think writing a thread safe referenced smart pointer is not hard maybe you should talk to the developers of the boost smart pointer and ask them what all the problems with implementing a normal smart pointer were.
Martin York
I don't need smart pointer semantics (I tend to avoid them anyway), the point is thread safe reference counting.
Gaspard Bucher
@Martin: You're right, apparently, boost::shared_ptr is thread safe; I didn't know that. From my experience it is really not that hard (and my implementation *is* thoroughly tested in the last two years :p). But indeed, if Boost supports it, I'd advise Gaspard to reuse that implementation. It's a good library. The only problem with it (from what I remember when using it some 5 years ago :p) is that the implementation is really hard to understand due to cross-compiler patches.
Dimitri C.
@Gaspard: There is not reason to be afraid of using smart pointers; don't let them scare you :p They merely provide a syntactic shorthand, and a thread safe smart pointer is a handy extension to your toolbox.
Dimitri C.
@Dimitri: I am building an embeddable library so boost is too much. My argument against smart pointers is that they "look" like pointers but behave differently (overhead): someone not familiar with the library might use them where they are not needed. Moreover, they tend to become overly complex header files that will be hard to maintain. If we are ready to loose speed for nicer syntax, let's just use Lua ;-).
Gaspard Bucher
+1  A: 

You are on the wrong track with this. Keep in mind: you can't lock data, you can only block code. You cannot protect the "objects" member with a locally defined mutex. You need the exact same mutex in the code that alters the objects collection. It must block that code when another thread is executing the call() method. The mutex must be defined at least at class scope.

Hans Passant
I understand "locking data" as "accessing the data from within a critical section", which is common practice.
Dimitri C.
Yes, a CS would work too. It still cannot be one that's local to the function, the code that alters "objects" needs to use the same one.
Hans Passant
Hmmm, I don't see any locally defined mutex. "mutex_" is a class member and ScopedRead is a scoped lock that locks "mutex_". There is no mutex instantiation in this code. Did I miss something ?
Gaspard Bucher
Okay, I didn't know what a ScopedLock was. Be sure to use "mutex_" also in the code that alters the "objects" member.
Hans Passant