Thread: Threading and mutex's

  1. #1
    Registered User
    Join Date
    Aug 2006
    Posts
    28

    Threading and mutex's

    I am building a multi-threaded c++ app using the boost::thread library. I am just a bit confused about when I should be using locks and mutex's. I understand that it is essential that global objects are locked before access, preventing two threads modifying the object at the same time. However, I was wondering when a mutex is actually needed.

    It goes without saying that it should be used when writing or modifying an object, but is it also needed when reading or copying? Any write operations will lock the object before hand, so there should be no danger of reading from an object at the same time as it is being written. Are there any dangers with simultanious reading of the same object from different threads?

  2. #2
    (?<!re)tired Mario F.'s Avatar
    Join Date
    May 2006
    Location
    Ireland
    Posts
    8,446
    Before anything else it must be said that not only global objets should be locked. Any object should be locked despite where it was declared. The object scope is not the focus of locking. If anything, it should be the object privileges.

    Anyways, if none of your threads are going to change the object, there is no need to lock it. To put it simply, Locking is a mechanism to avoid undefined behavior resulting from acessing an object that is being changed by another thread. If no thread changes the object, it is considered safe to access it without the need to lock it. A mutex achieves this by preventing thread access to an object that has been locked by another thread.

    However you must be very careful and probably adopt a proactive attitude towards locking. This is so because a) not always you will know if an object will be changed, and b) run-time errors and particularly undefined behavior from race conditions (the name given to the undefined behavior resulting from read/write unlocked names) are especially hard to debug.

    Basically, in case of doubt, lock. But if you are well aware that you are not going to change the resource, then there is no need to.

    One last word of advice...

    You must be aware that certain things we take for granted as hazy aspects of C++, are in fact anything but hazy. What I mean is that before multithreading we don't think much of certain facilities of the language and that will work against us when we get into this world. I know, I did recently and I'm constantly doing mistakes.

    Take std::cout for instance. If two threads use std::cout, it must be locked. Can you guess why?
    Originally Posted by brewbuck:
    Reimplementing a large system in another language to get a 25% performance boost is nonsense. It would be cheaper to just get a computer which is 25% faster.

  3. #3
    Registered User
    Join Date
    May 2003
    Posts
    1,619
    Quote Originally Posted by Mario F.
    Before anything else it must be said that not only global objets should be locked. Any object should be locked despite where it was declared. The object scope is not the focus of locking. If anything, it should be the object privileges.
    Well, a caveat there is that if an object is only "known" to one thread -- that is, if the other thread cannot access the object directly or indirectly -- then you don't need to lock on access.

    In general, for single variables of types bool and char, it's generally safe to not lock if only one thread can write the variable, as a change in the variable will be atomic. That may or may not be true of short, int, etc. because it will depend on how the bytes are aligned in memory.
    You ever try a pink golf ball, Wally? Why, the wind shear on a pink ball alone can take the head clean off a 90 pound midget at 300 yards.

  4. #4
    Registered User
    Join Date
    Aug 2006
    Posts
    28
    Thanks, that clears things up a lot. I can see why std::cout would need to be locked, as i'm guessing that allowing two threads to write to the output stream at one time would produce a mangled mess of garbage or maybe even crash the program.

    There is one thing that confuses me about boosts locking method. My understanding of locking would suggest that you initialise a lock and assign it to a specific resource, weather it be a stream or resource. Either that or initialise a lock and decide you will use it when acquiring a lock for certain resources. I imagine that a particular instance of the lock should be available to all files which have methods that make use of the shared resource that the lock is for. If this is correct then so far so good..

    I'm going to give an example of how I would use boost's mutexes. If you think my understanding is flawed then please let me know.

    Lets say you have a variable of string type called 'globalstr'. I'm guessing you would create a mutex specifically for this resource..
    Code:
    #include <boost/thread/mutex.hpp>
    boost::mutex globalstr_mutex;
    You could posibly define the mutex in a header file called 'mutexes.h'. If any file wants to use the resource this mutex is for then they should have included that header. To use the resource you would have to acquire a lock on that particular mutex. The tutorial on boost::threads here suggests the following method for acquiring locks..

    Code:
    #include "mutexes.h"
    #include <boost/thread/thread.hpp>
    #include <boost/thread/mutex.hpp>
    
    void someFunction() {
            boost::mutex::scoped_lock lock(globalstr_mutex);
            globalstr = globalstr + "more text";
           //more code...
    }
    But what confuses me at this point is how exactly the scoped lock works. In principal it sounds like a very safe locking method to use.
    To construct one of these types, pass in a reference to a mutex. The constructor locks the mutex and the destructor unlocks it. C++ language rules ensure the destructor will always be called, so even when an exception is thrown, the mutex will always be unlocked properly.
    It says that the scoped lock will always ensure that the mutex is always unlocked. What it doesnt explain is when the mutex is unlocked. Is it at the end of a block of code, or once the shared resource has been used (which doesnt make sense as you dont pass a reference to the resource when initialising the mutex). Does it only unlock when the function in which the lock was initialised stops execution and returns. If so, what if this function calls another function which requires a lock on the same mutex, possibly resulting in deadlock.

    I just want to clear these uncertainties up before diving head-first into thread programing.

  5. #5
    (?<!re)tired Mario F.'s Avatar
    Join Date
    May 2006
    Location
    Ireland
    Posts
    8,446
    Quote Originally Posted by megatron09
    There is one thing that confuses me about boosts locking method. My understanding of locking would suggest that you initialise a lock and assign it to a specific resource, weather it be a stream or resource. Either that or initialise a lock and decide you will use it when acquiring a lock for certain resources. I imagine that a particular instance of the lock should be available to all files which have methods that make use of the shared resource that the lock is for. If this is correct then so far so good..
    I'm not sure if I follow you here.
    You don't actually assign a mutex to a resource you want to share. A mutex object has one of two states; locked or unlocked. When a thread tries to access it and it is locked (because another thread locked it) one of 6(!) things can happen based on the type of mutex:

    Simple Mutex (boost::mutex):
    • Waits for another thread to unlock it and then locks (boost::mutex)
    • Tries to lock. If it is already locked, returns (boost::try_mutex)
    • Waits for another thread to unlock it, or until some amount of time elapses, then locks. (boost::timed_mutex)
    Recursive Mutex (boost::recursive_mutex):
    • Same as above. The difference is that recursive mutexes may be locked more than once by the same thread. An internal counter (I believe) takes care of how many times the mutex was locked. The thread has, of course, to unlock the mutex the same amount of times it locked. The names are boost::recursive_mutex, boost::recursive_try_mutex and boost::recursive_timed_mutex.

    I'm going to give an example of how I would use boost's mutexes. If you think my understanding is flawed then please let me know.
    The example is ok.

    But what confuses me at this point is how exactly the scoped lock works. In principal it sounds like a very safe locking method to use.
    It is... slightly... misleading. boost::mutex::scoped_lock will in fact create a scoped_lock object(!). Yup, and your mutex is associated to that object. The lock() function then locks it accordingly. This is how you actually lock according to the mutexes I described above.

    What you do basically is to first create one of the 6 mutexes described above, according to your needs. You now have a mutex. But you also need a lock object to associate that mutex with.

    The lock object is declared much the same way you declare iterators for container objects. Boost followed by the mutex type followed by the lock type (as oposed to std followed by the container type, followed by the iterator type).

    There are 5 locks. Check them here http://www.boost.org/doc/html/thread....concepts.Lock

    It says that the scoped lock will always ensure that the mutex is always unlocked. What it doesnt explain is when the mutex is unlocked. Is it at the end of a block of code, or once the shared resource has been used (which doesnt make sense as you dont pass a reference to the resource when initialising the mutex). Does it only unlock when the function in which the lock was initialised stops execution and returns. If so, what if this function calls another function which requires a lock on the same mutex, possibly resulting in deadlock.
    To be technical about it, the unlock occurs in the lock object destructor. So, everytime the destructor is ran for a given lock object, an unlock occurs. Depending on the type of mutex this may or may not free the mutex to be locked by another thread (a recursive mutex with 3 locks would move down to 2 locks, still not allowing other thread to lock the resource).

    So... you must look only at the lock object, not the mutex object. Everytime the former goes out of scope, for instance, an unlock occurs.

    I just want to clear these uncertainties up before diving head-first into thread programing.
    Sorry for not replying sooner. Been in and around the whole day. But I knew this to be a long reply so I kept pushing it to later in the day
    Last edited by Mario F.; 09-04-2006 at 07:08 PM.
    Originally Posted by brewbuck:
    Reimplementing a large system in another language to get a 25% performance boost is nonsense. It would be cheaper to just get a computer which is 25% faster.

  6. #6
    Registered User
    Join Date
    Aug 2006
    Posts
    28
    Thanks Mario, that helps a lot

    The reason I was so puzzled was due to me not understanding all the jargon just yet

    When 'destructors' and 'scope' were mentioned as being the conditions in which an object was unlocked, I tended to just scratch my head. After reading more about them I think I have a better understanding. If I want more control over exactly when the mutex is unlocked (rather than just waiting for it to go out of scope), is it possible to call the ~lock destructor directly.

    I.e..

    Code:
    #include "mutexes.h"
    #include <boost/thread/thread.hpp>
    #include <boost/thread/mutex.hpp>
    
    void someFunction() {
            boost::mutex::scoped_lock lock(globalstr_mutex);
            globalstr = globalstr + "more text";
           //more code...
          ~lock();
          //yet more code..
    }
    If thats right, then I think i'll have it nailed. Once again, thanks

  7. #7
    Registered User
    Join Date
    Jan 2005
    Posts
    7,366
    >> is it possible to call the ~lock destructor directly.
    No (at least, you shouldn't do that).

    Call lock.unlock() instead.

  8. #8
    Registered User
    Join Date
    May 2003
    Posts
    1,619
    It is possible to force a variable out of scope by putting it inside any pair of {}

    Here is an example:

    Code:
         int main(){
    
              int x;
    
              {
                   int y;
                   // Here, both x and y are in scope
    
              } // y goes out of scope here.  if it was a class, its destructor is called here.
    
              // Here only x is in scope
         }
    You ever try a pink golf ball, Wally? Why, the wind shear on a pink ball alone can take the head clean off a 90 pound midget at 300 yards.

  9. #9
    (?<!re)tired Mario F.'s Avatar
    Join Date
    May 2006
    Location
    Ireland
    Posts
    8,446
    You don't want to explicitly unlock a mutex, megatron09.

    The reason is simple. You have no control on what threads access the resource first, second or last. Multithreading programming is non deterministic. Your program will almost certainly run differently every time you call it. To explicitly unlock a mutex is almost guaranteed to bring you problems.

    I'm particularly worried with your comment in "If thats right, then I think i'll have it nailed." after you show a piece of code where you try to unlock a mutex. Let's see if I can make this more clear to you using that same piece of code:

    I'll trace to you program execution assuming you have two threads (Thread_1 and Thread_2) that are calling that SomeFunction() of yours.

    • 1st - Your global mutex is created.
    • 2nd - You enter main or something and create two threads, Thread_1 and Thread_2. Both call SomeFunction().
    • 3rd - You join() each.

    From here on Thread_1 and Thread_2 will execute concurrently.
    • 4th - Thread_1 happens to be the first. Inside SomeFunction() it meets the scoped_lock object and locks your global mutex.
    • 5th - Thread_2 is right behind. It meets the scoped_lock object too. It tries to lock it, but finds it already locked. It does nothing. Waits there for it to be unlocked.
    • 6th - Thread_1 finishes the code and meets the end of scope for the scoped_lock object. The dstructor for the scoped_lock object is called, freeing the mutex.
    • 7th - Thread_2 is still waiting. But threads are not omniscient. It has to check the scoped_lock object from time to time to know of its state. When it goes to check it, Thread_1 had just stepped in once again and found it free. It again locks it. Thread_2 is left there waiting again.
    • 8th - Thread_1 finishes once again freeing the lock.
    • 9th - Thread_2 now found it free. It finally gets to run the code after the scoped_lock object.
    • 10th - ... and so on. The order of execution will vary between them all the time. Sometimes one of the threads will run the code in sucession 2 or more times, sometimes it won't. But more important, they will do it differently everytime your program calls them.


    And so it goes. If you explicitly unlock the object you are adding to your problems. Not that it can't be done mind you. But Scoped Locking is more than just the name of that lock object you are using. It's an actual technique (a very valuable one) in multi-threading, in which mutexes are locked in the lock object constructor and unlocked in its destructor. This not only frees the programmer from remembering to lock and unlock the mutex, but more importantly, it works much better in the presence of exceptions. Exceptions gurantee that your destructors will be called and as such, your mutex will be correctly unlocked.

    I can't think of a reason for anyone wanting to explicitly unlock a mutex when using Scoped Locking techniques. But I'm sure someone can point one or two reasons they would want to do that. My knowledge of multi-threading is still limited. So I'm not the best advisor on that one.
    Originally Posted by brewbuck:
    Reimplementing a large system in another language to get a 25% performance boost is nonsense. It would be cheaper to just get a computer which is 25% faster.

  10. #10
    Registered User
    Join Date
    Jan 2005
    Posts
    7,366
    I don't understand the point of your post Mario F. When you define the scope of the scoped_lock, you are determining when it will be unlocked. All that using unlock explicitly does is also to determine when it will be unlocked. Taking advantage of the automatic unlock is most appropriate in most places, but that doesn't mean that the explicit call is a bad idea. And it certainly has nothing to do with the fact that you don't have control over which threads access resources first.

    You use a lock to make sure there isn't concurrent access to a resource. As long as the unlock comes after that resource is finished being used, it doesn't matter whether it is explicit or not. A common technique is to add an extra scope around the important resource access to allow the destructor's implicit unlock. But that is really the same as adding the explicit unlock in terms of the effectiveness of the lock. All that discussion of which thread comees in which order is irrelevant to the question of explicit versus implicit unlocking.

  11. #11
    Registered User
    Join Date
    May 2003
    Posts
    1,619
    I tend to agree, although I think the extra scope helps improve readability. Another set of braces and an indentation will help to make it visually very obvious which resource accesses are being synchronized.
    You ever try a pink golf ball, Wally? Why, the wind shear on a pink ball alone can take the head clean off a 90 pound midget at 300 yards.

  12. #12
    Registered User
    Join Date
    Jan 2005
    Posts
    7,366
    BTW, in cases where separating the code into its own function is superfluous, I also would prefer the extra scope instead of the explicit unlock for those reasons.

  13. #13
    Registered User
    Join Date
    Aug 2006
    Posts
    28
    I appreciate your advice Mario, but I just wanted to know incase there was a situation where I had to explicitly unlock a mutex. I doubt there is, and i'm sure another method could be devised where this wasnt necessary, but at the same time i'm sure there are situations where a scoped lock could cause some kind of deadlock. The example I am about to give is most likely VERY bad code, especially for multi-threading, but nevertheless demonstrates an example of my point.

    You have 2 threads, thread1 and thread2. You also have 2 shared objects, obj1 and obj2. You therefor have 2 mutexes, obj1_mutex and obj2_mutex.

    Here is the code for thread one...
    Code:
    #include "mutexes.h"
    #include <boost/thread/thread.hpp>
    #include <boost/thread/mutex.hpp>
    
    void someFunction1() {
            boost::mutex::scoped_lock lock(obj1_mutex);
            change(&obj1);
            anotherFunction1();
            //more code...
    }
    
    void anotherFunction1(){
           boost::mutex::scoped_lock lock(obj2_mutex);
           change(&obj2);
           return;
    }
    And heres the code for thread2...
    Code:
    #include "mutexes.h"
    #include <boost/thread/thread.hpp>
    #include <boost/thread/mutex.hpp>
    
    void someFunction2() {
            boost::mutex::scoped_lock lock(obj2_mutex);
            change(&obj2);
            anotherFunction2();
            //more code...
    }
    
    void anotherFunction2(){
           boost::mutex::scoped_lock lock(obj1_mutex);
           change(&obj1);
           return;
    }
    And heres a possible scenario of what may happen at runtime...

    -thread1 begins execution.
    -thread1 acquires lock for obj1
    -thread2 begins exectuion.
    -thread2 acquires lock for obj2
    -thread1 calls a function which requires a lock on obj2.
    -thread2 already has a lock on obj2, so it waits..
    -thread2 calls a function which requires a lock on obj1.
    -thread1 already has a lock on obj1 (since the lock hasnt gone out of scope yet), so it also waits..
    -and waits..
    -and waits....

    As I said, this is probably extremely bad programming, and I cant think of a real-world example where the above algorithms would be needed. However, as my threaded program becomes more complex, whos to say that I might not code something like that accidently. It could certainly be debugged easily, but the errors are likely to occur at runtime, which would make it tricky to trace the problem. Surely overall it is safer to call the destructor on the lock once the shared resource for which the lock was called has been used. Maybe not, i'm not sure tbh.

    You know more on this subject than me, so i'm not saying youre wrong. Just pointing out why I asked the question

  14. #14
    (?<!re)tired Mario F.'s Avatar
    Join Date
    May 2006
    Location
    Ireland
    Posts
    8,446
    Quote Originally Posted by Daved
    I don't understand the point of your post Mario F. When you define the scope of the scoped_lock, you are determining when it will be unlocked. All that using unlock explicitly does is also to determine when it will be unlocked. Taking advantage of the automatic unlock is most appropriate in most places, but that doesn't mean that the explicit call is a bad idea.
    I would have to agree with you Daved. Especially in the case where the resource being controlled is just a tiny part of a vast function.

    However, with Scoped Locking I prefer to suggest anyone to avoid explicit unlocks and redesign their code, creating functions around the shared resources instead. This will offer a lot more control both during debug and maintenance... at the expense perhaps of loss of clarity and probably more complexity being added to the overall code structure.

    I have no right answers. But, you are right nonetheless. It can make sense, and will eventually do sooner or later, to explicitly unlock a mutex.

    And it certainly has nothing to do with the fact that you don't have control over which threads access resources first.
    It was badly positioned in the overal text. I suspected megatron09 was not really understanding yet what is involved with mutexes, lock objects, and threads. And it was 4 am in the morning...

    In fact his post seems to indicate the contrary. I shouldn't have skimmed throught it.
    Last edited by Mario F.; 09-07-2006 at 03:10 PM.
    Originally Posted by brewbuck:
    Reimplementing a large system in another language to get a 25% performance boost is nonsense. It would be cheaper to just get a computer which is 25% faster.

  15. #15
    (?<!re)tired Mario F.'s Avatar
    Join Date
    May 2006
    Location
    Ireland
    Posts
    8,446
    Quote Originally Posted by megatron09
    As I said, this is probably extremely bad programming, and I cant think of a real-world example where the above algorithms would be needed.
    Maybe not exactly like that, but deadlocking is quiet common nonetheless.

    The thing with multi-threading is that in the end of the day we will have to compromise. Under more complex situations, we will have to forsake clarity in order to solve a given problem. Multi-threading is, after all, already a different way of programming, so we are better off understanding that under certains situations we will have to change our normal way of doing things.

    The answer to your specific problem is thus twofold. On one hand you must avoid those situations in your code. Basically, don't code for them. Try to change your code logic so that you don't get yourself into them. But on those rare instances where you will have no other choice there's a few mechanisms you can probably use. One I can think of the top of my head is...

    Condition Variables (boost::condition). These allow you to use a shared resource conditionally. The thread locks the mutex and then checks the resource to see if it can be used according to the rules you established (through a condition object). If it can't be used, the thread unlocks the mutex (effectively allowing the resource to be changed by another thread) and waits for the resource to become valid.
    Originally Posted by brewbuck:
    Reimplementing a large system in another language to get a 25% performance boost is nonsense. It would be cheaper to just get a computer which is 25% faster.

Popular pages Recent additions subscribe to a feed