Thread: Am I doing the locking correctly?

  1. #1
    [](){}(); manasij7479's Avatar
    Join Date
    Feb 2011
    Location
    *nullptr
    Posts
    2,657

    Am I doing the locking correctly?

    My program here, tries to make a tread safe stack by using locks.

    This was quite simple.. but I want to know if I'm making a conceptual mistake.
    (Is using different mutex (s) for two data members which may not be accessed in the same function, a good idea?)

    The test program spawns 6 threads pushing a number into the same stack, 500 times each.
    And after joining them I check that the stack's size is 3000.
    (Without the locking the number is about ~200 less !!..that gives me some confidence about the correctness of the code.)

    Test code:
    Code:
    #include<thread>
    #include<mutex>
    #include<iostream>
    #include<array>
    #include<exception>
    #include<functional>
    #include<vector>
    int main()
    {
        mm::stack<int,10000> si;
        
        auto foo = [](mm::stack<int,10000>& si,int n){for(int i=0;i<500;i++)si.push(n);};
        //^ Pushes n 500 times into the stack si.
        
        std::vector<std::thread> threads;
        for(int i=0;i<6;i++)
            threads.push_back(std::thread(foo,std::ref(si),i));
        for(auto&x:threads)x.join();
        
        si.debug_print();
    }
    The stack:
    Code:
    namespace mm
    {
        template<typename T, std::size_t MAX=10>
        class stack
        {
            typedef std::lock_guard<std::mutex> lgm;
        public:
            stack():head(-1){};
            bool empty()
            {
                lgm l(head_lock);
                return (head==-1);
            }
            T top()
            {
                if(empty())throw(std::runtime_error("Empty"));
                
                lgm d(data_lock),h(head_lock);
                return data[head];
            }
            std::size_t size()
            {
                lgm l(head_lock);
                return head+1;
            }
            void push(const T& t)
            {
                if(size()==MAX)throw(std::runtime_error("Overflow"));
                
                lgm d(data_lock),h(head_lock);
                data[++head]=t;
            }
            void pop()
            {
                if(empty())throw(std::runtime_error("Underflow"));
                lgm h(head_lock);
                --head;
            }
            void debug_print()
            {
                for(int i=0;i<=head;i++)
                    std::cout<<data[i];
                std::cout<<"\nSize: "<<head+1;
            }
        private:
            std::array<T,MAX> data;
            int head;
            std::mutex data_lock;
            std::mutex head_lock;
            
        };
    }
    Last edited by manasij7479; 06-09-2012 at 06:27 AM.

  2. #2
    Registered User
    Join Date
    Jun 2005
    Posts
    6,815
    Not quite.

    There is a general principle in multi-threaded design which says that no object (and entity that contains data) can protect itself from concurrent access.

    To illustrate how that principle fails in your code, consider what happens if two threads are concurrently pushing a value to a stack that has size() equal to MAX-1. This sequence is feasible.
    Code:
          thread A evaluates size(), detects the value is MAX-1, so does not throw an exception
          thread B evaluates size(), detects the value is MAX-1, so does not throw an exception.
          thread B adds a value to the stack.   The size is now MAX.
          thread A adds a value to the stack.  The size is now MAX+1.
    From here on in, unless elements are popped, push() will continue growing the stack forever.

    Although it might seem counter-intuitive, this sort of thing isn't fixed by just ensuring every member function grabs a mutex at the beginning, and releases it before returning. You need to have the thread function that is calling push() (or other operations) make use of an appropriate mutex. Having a mutex (or more than one mutex) managed by the same object that manages other data is insufficient.
    Right 98% of the time, and don't care about the other 3%.

    If I seem grumpy or unhelpful in reply to you, or tell you you need to demonstrate more effort before you can expect help, it is likely you deserve it. Suck it up, Buttercup, and read this, this, and this before posting again.

  3. #3
    [](){}(); manasij7479's Avatar
    Join Date
    Feb 2011
    Location
    *nullptr
    Posts
    2,657
    I didn't think of that !

    So, my foo function should be like this:
    Code:
    std::mutex m; //Is there a difference between this being a global and a static local?
    void foo(mm::stack<int,10000>& si,int n)
    {
        for(int i=0;i<500;i++)
        {
            m.lock();
            si.push(n);
            m.unlock();
        }
    };
    ?
    and I could use a normal stack ?


    [For some reason, this is refusing to compile, despite having the same signature as the lambda......[edit]It was a typo..wrote 10000 instead of 3000 (which is my changed MAX).[/edit] ]
    Last edited by manasij7479; 06-09-2012 at 07:13 AM.

  4. #4
    Registered User
    Join Date
    Jun 2005
    Posts
    6,815
    Quote Originally Posted by manasij7479 View Post
    So, my foo function should be like this:
    Code:
    std::mutex m; //Is there a difference between this being a global and a static local?
    void foo(mm::stack<int,10000>& si,int n)
    {
        for(int i=0;i<500;i++)
        {
            m.lock();
            si.push(n);
            m.unlock();
        }
    };
    ?
    and I could use a normal stack ?
    Assuming all other functions that do anything to the stack use the same mutex, yes.

    (Sorry, bit rushed, so haven't looked at the compile errors ... I'll leave that for someone else to take on)
    Right 98% of the time, and don't care about the other 3%.

    If I seem grumpy or unhelpful in reply to you, or tell you you need to demonstrate more effort before you can expect help, it is likely you deserve it. Suck it up, Buttercup, and read this, this, and this before posting again.

  5. #5
    [](){}(); manasij7479's Avatar
    Join Date
    Feb 2011
    Location
    *nullptr
    Posts
    2,657
    Quote Originally Posted by grumpy View Post
    Assuming all other functions that do anything to the stack use the same mutex, yes.
    Thanks....though I wonder where the term "thread safe data structure" comes from....if everything is up to the code using it.
    (Sorry, bit rushed, so haven't looked at the compile errors ... I'll leave that for someone else to take on)
    It was a typo.

  6. #6
    Registered User antred's Avatar
    Join Date
    Apr 2012
    Location
    Germany
    Posts
    257
    Well, you could build a wrapper class around that stack that implements its own locking in its pop() und push() methods. That class could then be said to be "thread-safe".


    P.S. Oh never mind, you did that in your original code ... there are just some holes in it.
    Last edited by antred; 06-09-2012 at 08:35 AM.

  7. #7
    Algorithm Dissector iMalc's Avatar
    Join Date
    Dec 2005
    Location
    New Zealand
    Posts
    6,318
    Your pop has a problem anyway. You first call empty, which does a lock, fetches the answer and then unlocks. Then you re-lock to decrement the head.
    Between that unlock and the subsequent re-lock another thread may empty the stack.
    You would need to place your lock 'h' before the empty() call, assuming that type of lock allows recursive locking.

    'top' has the same problem.

    And as noted, you can't actually use this class safely anyway because to remove the item at the top requires a call to top and pop, and to keep it locked over the duration of both calls requires that the locking be done externally. Well either that or you add the ability for the external code to call methods that lock and unlock on this class, again relying on the external code to get it right.
    My homepage
    Advice: Take only as directed - If symptoms persist, please see your debugger

    Linus Torvalds: "But it clearly is the only right way. The fact that everybody else does it some other way only means that they are wrong"

  8. #8
    Master Apprentice phantomotap's Avatar
    Join Date
    Jan 2008
    Posts
    5,108
    I wonder where the term "thread safe data structure" comes from....if everything is up to the code using it.
    O_o

    Because "There is a general principle in multi-threaded design which says that no object can protect itself from concurrent access." is an abstract guideline to understanding concurrency not an actual rule. If you don't think on it when you set out to design a new implementation you are very likely to forget important concerns.

    Those data structures which are thread safe in and of themselves are either built from naturally isolated concerns (rare and generally unprovable) or combine several bits of functionality into atomic transactions (common as dirt).

    I'll ignore the first bit because I've yet to be convinced that anything like that is possible for complex data structures.

    The second one is generally easy in that the methodology required is easy to grasp but difficult in that in often means making "trade-offs" that you will not immediately understand or even be comfortable with making.

    The second one is something you can do here to make the use of the class correct without manually locking. The idea would require you to sacrifice some measure of exception neutrality and a pretty drastic change in the interface. (Which, by the by, will not work the way you probably want in the real world.)

    So, for example, the scenarios where this breaks is because you've isolated functionality into foundation components and then built on that foundation to provide more complex functionality. This is excellent design in general. This will never work if your goal is to make "self safe" complex data structures. You'll have to combine multiple bits of high level functionality into a single point of entrance (which will protect itself with a matrix shared by all references to a given instance). This point of entrance can then be used by interface functions for the convenience of simplicity. This isn't the only way; it is just one way that is easy to explain.

    To elaborate on the concerns here, if you move all the necessary mechanic for `empty', `top', `size', `push', and `pop' into a single function you still can't ensure the safety of the operations because of the interface you've employed; you would still have the same outside problems. To correct these issues you'd need to combine operations for atomic transactions. In other words, every outside operation built from multiple primitives must be eliminated by combining them into a single combined method. One example would be yielding the last element to a caller for use; this operation as it is written needs both `top' and `pop' so requires outside locking to be thread safe; this operation needs to combined into a single `popfrom' or such method.

    In a way, designing for concurrency is exactly like designing for exceptions. It can't be slapped on as an afterthought and get any kind of good results. The issues have to be considered from the start.

    Soma

  9. #9
    Registered User
    Join Date
    Jun 2005
    Posts
    6,815
    Quote Originally Posted by manasij7479 View Post
    Thanks....though I wonder where the term "thread safe data structure" comes from....if everything is up to the code using it.
    The word "ignorance" often comes to mind as an explanation for that.

    However, there are certain things like atomic types, which are managed by the operating system. Essentially, the OS provides an API (i.e. protective code) and manages things from there. User code makes requests to the OS. However, it is possible to assemble unsafe code using atomic operations. The issue is one of design.
    Last edited by grumpy; 06-09-2012 at 04:44 PM.
    Right 98% of the time, and don't care about the other 3%.

    If I seem grumpy or unhelpful in reply to you, or tell you you need to demonstrate more effort before you can expect help, it is likely you deserve it. Suck it up, Buttercup, and read this, this, and this before posting again.

  10. #10
    Master Apprentice phantomotap's Avatar
    Join Date
    Jan 2008
    Posts
    5,108
    The word "ignorance" often comes to mind as an explanation for that.
    ^_^

    I blame some of the earliest Java documentation where people heard "thread safe data structure" and thought they said "don't worry you can't mess this up with threads" despite that same documentation using synchronization primitives in literally every code example where the client code needed to use two or more interfaces to perform an operation.

    Soma

  11. #11
    Algorithm Dissector iMalc's Avatar
    Join Date
    Dec 2005
    Location
    New Zealand
    Posts
    6,318
    That got me thinking... One way this kind of thing might be able to work is if you design the interface to allow passing in lamdas / std::functions. Then your methods can lock, perform some actions according to the lambdas passed in, and then unlock and exit. You still rely on certain things about those lambdas or functions such as not being written in a way that causes a deadlock.
    Essentially the callers work is done inside the callee's code. I don't think this would work very well in practice. It would probably be annoying to use and somewhat limiting in what you can do. I'm not even going to try and come up with some sample code for it. It's probably some kind of anti-pattern.
    My homepage
    Advice: Take only as directed - If symptoms persist, please see your debugger

    Linus Torvalds: "But it clearly is the only right way. The fact that everybody else does it some other way only means that they are wrong"

  12. #12
    Master Apprentice phantomotap's Avatar
    Join Date
    Jan 2008
    Posts
    5,108
    Then your methods can lock, perform some actions according to the lambdas passed in, and then unlock and exit.
    That entire thing sounds like an attempt to get "execute around pointer" without the benefit of C++ features like "RAII".

    Soma

  13. #13
    [](){}(); manasij7479's Avatar
    Join Date
    Feb 2011
    Location
    *nullptr
    Posts
    2,657
    Quote Originally Posted by iMalc View Post
    You would need to place your lock 'h' before the empty() call, assuming that type of lock allows recursive locking.
    As empty itself has a lock (locking the same mutex object)... won't that result in deadlocks ?
    (That was my reasoning for not doing what you say .)

    [Edit: This may be solved with passing/moving the mutex object and calling std::lock with the extra argument std::adopt_lock ...but I'm absolutely not sure about this.]
    Last edited by manasij7479; 06-10-2012 at 01:20 AM.

  14. #14
    Registered User
    Join Date
    Jun 2005
    Posts
    6,815
    Quote Originally Posted by manasij7479 View Post
    As empty itself has a lock (locking the same mutex object)... won't that result in deadlocks ?
    That depends on which type of mutex you use. Under C++-11, std::recursive_mutex can be locked multiple times by one thread (the effect is increasing a lock count, so it needs to be unlocked the same number of times). std::mutex cannot (necessarily) do that.

    With other libraries (posix, win32, etc) there are generally options to either create different types of synchronisation primitives (mutexes, critical sections, etc) with relevant properties.
    Right 98% of the time, and don't care about the other 3%.

    If I seem grumpy or unhelpful in reply to you, or tell you you need to demonstrate more effort before you can expect help, it is likely you deserve it. Suck it up, Buttercup, and read this, this, and this before posting again.

  15. #15
    C++まいる!Cをこわせ!
    Join Date
    Oct 2007
    Location
    Inside my computer
    Posts
    24,654
    I thought a lot about thing some time ago when I had to implement thread safe applications.
    Generally, when doing thread safety, there is, except for the simple lock whenever two threads use the same object, another thing you have to keep in mind: assumptions.
    In a threaded environment, serial code execution--something that we rely on everyday--no longer holds. You can't rely on it.
    Assume that every function is "thread safe," that is, it locks to ensure mutual exclusion in every member function. Consider:

    Thread 1:
    Code:
    if (stack.empty())
    	stack.push("Hello World!");
    Thread 2:
    Code:
    stack.push("I like this world!");
    stack.push("I hate this world!");
    stack.pop();
    Totally safe code, right (if members functions lock)?
    That's what you'd think, but it isn't in a threaded environment.
    "Hello World!" should be pushed if and only if the stack is empty. The if statement is okay, but as soon as it ends all bets are off. You can't guarantee that the stack will be empty when the push is made (thread 2 can execute push before thread 1 does a push). Were it a single threaded application, then it would hold true, but not in a multi-threaded app.
    So when building multi-threaded applications, you must take into account how long you need your assumptions to be valid.

    So the correct code should be:
    Thread 1:
    Code:
    lock();
    if (stack.empty())
    	stack.push("Hello World!") // Make sure the the empty state still holds here!
    unlock();
    Thread 2:
    Code:
    lock();
    stack.push("I like this world!"); // Just to make sure thread 1's lock works.
    unlock();
    lock();
    stack.push("I hate this world!");
    stack.pop(); // We must ensure that "I hate this world!" was the last thing pushed onto the stack.
    unlock();
    Quote Originally Posted by iMalc View Post
    That got me thinking... One way this kind of thing might be able to work is if you design the interface to allow passing in lamdas / std::functions. Then your methods can lock, perform some actions according to the lambdas passed in, and then unlock and exit. You still rely on certain things about those lambdas or functions such as not being written in a way that causes a deadlock.
    Essentially the callers work is done inside the callee's code. I don't think this would work very well in practice. It would probably be annoying to use and somewhat limiting in what you can do. I'm not even going to try and come up with some sample code for it. It's probably some kind of anti-pattern.
    I actually made a container wrapper that you would actually have to lock before you can access its underlying object.
    Of course, the lock function returned another object whose destructor would unlock. I kind of liked the solution.
    Last edited by Elysia; 06-10-2012 at 08:11 AM.
    Quote Originally Posted by Adak View Post
    io.h certainly IS included in some modern compilers. It is no longer part of the standard for C, but it is nevertheless, included in the very latest Pelles C versions.
    Quote Originally Posted by Salem View Post
    You mean it's included as a crutch to help ancient programmers limp along without them having to relearn too much.

    Outside of your DOS world, your header file is meaningless.

Popular pages Recent additions subscribe to a feed

Similar Threads

  1. Locking up!
    By Incantrix in forum Game Programming
    Replies: 6
    Last Post: 12-11-2010, 01:16 AM
  2. Locking down the computer
    By zacs7 in forum Tech Board
    Replies: 6
    Last Post: 02-28-2009, 05:48 AM
  3. System locking
    By Shadow in forum Tech Board
    Replies: 12
    Last Post: 11-02-2005, 05:37 PM
  4. file locking over web
    By puZZles in forum C Programming
    Replies: 1
    Last Post: 06-14-2003, 07:50 PM
  5. mutex locking
    By quagsire in forum Linux Programming
    Replies: 3
    Last Post: 11-29-2002, 10:47 AM