开发者

Modelling boost::Lockable with semaphore rather than mutex (previously titled: Unlocking a mutex from a different thread)

开发者 https://www.devze.com 2022-12-29 02:37 出处:网络
I\'m using the C++ boost::thread library, which in my case means I\'m using pthreads.Officially, a mutex must be unlocked from the same thread which locks it, and I want the effect of being able to lo

I'm using the C++ boost::thread library, which in my case means I'm using pthreads. Officially, a mutex must be unlocked from the same thread which locks it, and I want the effect of being able to lock in one thread and then unlock in another. There are many ways to accomplish this. One possibility would be to write a new mutex class which allows this behavior.

For example:

class inter_thread_mutex{
  bool locked;
  boost::mutex mx;
  boost::condition_variable cv;
public:
  void lock(){
    boost::unique_lock<boost::mutex> lck(mx);
    while(locked) cv.wait(lck);
    locked=true;
  }

  void unlock(){
    {
      boost::lock_guard<boost::mutex> lck(mx);
      if(!locked) error();
      locked=false;
    }
    cv.notify_one();
  }
// bool try_lock(); void error(); etc.
}

I should point out that the above code doesn't guarantee FIFO access, since if one thread calls lock() while another calls unlock(), this first thread may acquire the lock ahead of other threads which are waiting. (Come to think of it, the boost::thread documentation doesn't appear to make any explicit scheduling guarantees for either mutexes or condition variables). But let's just ignore that (and any other bugs) for now.

My question is, if I decide to go this route, would I be able to use such a mutex as a model for the boost Lockable concept. For example, would anything go wrong if I use a boost::unique_lock< inter_thread_mutex > for RAII-style access, and then pass this lock to boost::condition_variable_any.wait(), etc.

On one hand I don't see why not. On the other hand, "I don't see why not" is usually a very bad way of determining whether something will work.

The reason I ask is that if it turns out that I hav开发者_如何转开发e to write wrapper classes for RAII locks and condition variables and whatever else, then I'd rather just find some other way to achieve the same effect.

EDIT: The kind of behavior I want is basically as follows. I have an object, and it needs to be locked whenever it is modified. I want to lock the object from one thread, and do some work on it. Then I want to keep the object locked while I tell another worker thread to complete the work. So the first thread can go on and do something else while the worker thread finishes up. When the worker thread gets done, it unlocks the mutex.

And I want the transition to be seemless so nobody else can get the mutex lock in between when thread 1 starts the work and thread 2 completes it.

Something like inter_thread_mutex seems like it would work, and it would also allow the program to interact with it as if it were an ordinary mutex. So it seems like a clean solution. If there's a better solution, I'd be happy to hear that also.

EDIT AGAIN: The reason I need locks to begin with is that there are multiple master threads, and the locks are there to prevent them from accessing shared objects concurrently in invalid ways. So the code already uses loop-level lock-free sequencing of operations at the master thread level. Also, in the original implementation, there were no worker threads, and the mutexes were ordinary kosher mutexes.

The inter_thread_thingy came up as an optimization, primarily to improve response time. In many cases, it was sufficient to guarantee that the "first part" of operation A, occurs before the "first part" of operation B. As a dumb example, say I punch object 1 and give it a black eye. Then I tell object 1 to change it's internal structure to reflect all the tissue damage. I don't want to wait around for the tissue damage before I move on to punch object 2. However, I do want the tissue damage to occur as part of the same operation; for example, in the interim, I don't want any other thread to reconfigure the object in such a way that would make tissue damage an invalid operation. (yes, this example is imperfect in many ways, and no I'm not working on a game)

So we made the change to a model where ownership of an object can be passed to a worker thread to complete an operation, and it actually works quite nicely; each master thread is able to get a lot more operations done because it doesn't need to wait for them all to complete. And, since the event sequencing at the master thread level is still loop-based, it is easy to write high-level master-thread operations, as they can be based on the assumption that an operation is complete (more precisely, the critical "first part" upon which the sequencing logic depends is complete) when the corresponding function call returns.

Finally, I thought it would be nice to use inter_thread mutex/semaphore thingies using RAII with boost locks to encapsulate the necessary synchronization that is required to make the whole thing work.


man pthread_unlock (this is on OS X, similar wording on Linux) has the answer:

NAME
     pthread_mutex_unlock -- unlock a mutex

SYNOPSIS
     #include <pthread.h>

     int
     pthread_mutex_unlock(pthread_mutex_t *mutex);

DESCRIPTION
     If the current thread holds the lock on mutex, then the
     pthread_mutex_unlock() function unlocks mutex.

     Calling pthread_mutex_unlock() with a mutex that the
     calling thread does not hold will result in
     undefined behavior.

     ...

My counter-question would be - what kind of synchronization problem are you trying to solve with this? Most probably there is an easier solution.

Neither pthreads nor boost::thread (built on top of it) guarantee any order in which a contended mutex is acquired by competing threads.


Sorry, but I don't understand. what will be the state of your mutex in line [1] in the following code if another thread can unlock it?

inter_thread_mutex m;

{
  m.lock();
  // [1]
  m.unlock();
}

This has no sens.


There's a few ways to approach this. Both of the ones I'm going to suggest are going to involve adding an additional piece of information to the object, rather adding a mechanism to unlock a thread from a thread other than the one that owns it.

1) you can add some information to indicate the object's state:

enum modification_state { consistent, // ready to be examined or to start being modified
                          phase1_complete, // ready for the second thread to finish the work
                        };

// first worker thread
lock();
do_init_work(object);
object.mod_state = phase1_complete;
unlock();
signal();
do_other_stuff();

// second worker thread
lock()
while( object.mod_state != phase1_complete )
  wait()
do_final_work(obj)
object.mod_state = consistent;
unlock()
signal()

// some other thread that needs to read the data
lock()
while( object.mod_state != consistent )
  wait();
read_data(obj)
unlock()

Works just fine with condition variables, because obviously you're not writing your own lock.

2) If you have a specific thread in mind, you can give the object an owner.

  // first worker
  lock();
  while( obj.owner != this_thread() ) wait();
  do_initial_work(obj);
  obj.owner = second_thread_id;
  unlock()
  signal()

  ...

This is pretty much the same solution as my first solution, but more flexible in the adding/removing of phases, and less flexible in the adding/removing of threads.

To be honest, I'm not sure how inter thread mutex would help you here. You'd still need a semaphore or condition variable to signal the passing of the work to the second thread.


Small modification to what you already have: how about storing the id of the thread which you want to take the lock, in your inter_thread_whatever? Then unlock it, and send a message to that thread, saying "I want you execute whatever routine it is that tries to take this lock".

Then the condition in lock becomes while(locked || (desired_locker != thisthread && desired_locker != 0)). Technically you've "released the lock" in the first thread, and "taken it again" in the second thread, but there's no way that any other thread can grab it in between, so it's as if you've transferred it directly from one to the other.

There's a potential problem, that if a thread exits or is killed, while it's the desired locker of your lock, then that thread deadlocks. But you were already talking about the first thread waiting for a message from the second thread to say that it has successfully acquired the lock, so presumably you already have a plan in mind for what happens if that message is never received. To that plan, add "reset the desired_locker field on the inter_thread_whatever".

This is all very hairy, though, I'm not convinced that what I've proposed is correct. Is there a way that the "master" thread (the one that's directing all these helpers) can just make sure that it doesn't order any more operations to be performed on whatever is protected by this lock, until the first op is completed (or fails and some RAII thing notifies you)? You don't need locks as such, if you can deal with it at the level of the message loop.


I don't think it is a good idea to say that your inter_thread_mutex (binary_semaphore) can be seen as a model of Lockable. The main issue is that the main feature of your inter_thread_mutex defeats the Locakble concept. If inter_thread_mutex was a model of lockable you will expect in In [1] that the inter_thread_mutex m is locked.

// thread T1
inter_thread_mutex m;

{
  unique_lock<inter_thread_mutex> lk(m);
  // [1]
}

But as an other thread T2 can do m.unlock() while T1 is in [1], the guaranty is broken.

Binary semaphores can be used as Lockables as far as each thread tries to lock before unlocking. But the main goal of your class is exactly the contrary.

This is one of the reason semaphores in Boost.Interprocess don't use lock/unlock to name the functions, but wait/notify. Curiously these are the same names used by conditions :)


A mutex is a mechanism for describing mutually exclusive blocks of code. It does not make sense for these blocks of code to cross thread boundaries. Trying to use such a concept in such an counter intuitive way can only lead to problems down the line.

It sounds very much like you're looking for a different multi-threading concept, but without more detail it's hard to know what.

0

精彩评论

暂无评论...
验证码 换一张
取 消