views:

765

answers:

7

Hi All,

In a recent interview, I was asked to implement a thread safe generic (i.e.template based) stack in C++, on linux machine.
I quickly came up with the following (It may have compilation errors).
I got through. The interviewer probably liked something in this implementation. Maybe the design part :)
Here are a few problems that this implementation may have:-
1. Incorrect implementation to indicate overflow/underflow. There is no overflow handling since I'm using STL vector as the underlying data structure. Should there be any such handling? Also, underflow (in Pop()) yields false as return value. Should it be done by throwing of an exception?
2. Implementation of PopElem routine. Is the below implementation correct?
3. No real use of top element.
4. Better timing between start of writer and reader thread.

Please make any comments/suggestions/improvements.
Thanks.

//Implementing a thread safe generic stack.

#include<pthread.h>
#include<iostream>
#include<vector>

using namespace std;

template<typename T>
class MyStack
{
public:
//interface
bool Push(T elem);
bool Pop(T& elem);
bool IsEmpty();

//constructor
MyStack() {
pthread_mutex_init(&lock);
top = 0;
}

//destructor
~MyStack() {
pthread_mutex_destroy(&lock);
}

private:
pthread_mutex_t lock;
int top;
vector<T> stack;

bool MyStack::Push(T elem);
bool MyStack::PopElem(T& elem);
}; //end of MyStack

template<typename T>
bool MyStack<T>::Push(T elem)
{
    pthread_mutex_lock(&lock);
    PushElem(elem);
    pthread_mutex_unlock(&lock);
}

template<typename T>
bool MyStack<T>::Pop(T& elem)
{
    pthread_mutex_lock(&lock);
    PopElem(elem);
    pthread_mutex_unlock(&lock);
}

template<typename T>
bool MyStack<T>::PushElem(T elem)
{
    stack.push_back(elem);
     top = stack.size();
}

template<typename T>
bool MyStack<T>::PopElem(T& elem)
{
   if(this.IsEmpty())
   {
        return false;
   }

   elem = stack.back(); //tricky, returns a reference to the last element
   stack.pop_back(); // is elem valid after this ??
   top = stack.size();
   return true;
}      


template<typename T>
bool MyStack<T>::IsEmpty()
{
    return stack.empty();
}


class MyStackTest
{
public:
  void Initialize() {
  pthread_init(&readerT);
  pthread_init(&writerT);
  }

  void Run() {
 pthread_create(writerT,0,writer,0); 
 pthread_create(readerT,0,reader,0);
 pthread_join(&writerT);
 pthread_join(&readerT);
}

private:
pthread_t readerT;
pthread_t writerT;
MyStack<int> stack;

void reader(void);
void writer(void);
};

void MyStackTest::writer() {
  for(int i=0;i<20;i++) {
      stack.Push(i);
      cout<<"\n\t Pushed element: "<<i;
   } //end for
}

void MyStackTest::reader() {
   int elem;
   while(stack.Pop(elem))
   {
     cout<<"\n\t Popped: "<<elem;
   }
}

int main()
{
    MyStackTest Test;

    Test.Run();
}
+8  A: 

Some issues:

  • I would implement a Locker class to claim & free the mutex using RAII
  • I would use std::stack
  • I would make the user of std::stack use the Locker to implement the locking policy - having a stack that locks itself is bad design, as the stack can't know how it is to be used
anon
"having a stack that locks itself is bad design". How do you know? If the user of a stack has discovered that he always makes push and pop atomic, and needs no other atomic operations, why is it "bad design" to encapsulate this requirement? Does this rule apply to stacks, or to other collections? Is having a message queue that locks itself a bad design, or should users of message queues be forbidden from encapsulating their thread-safety? How high does this prohibition go? And so on with the whinging. You're absolutely right, though, that he should just wrap std::stack and copy its interface.
Steve Jessop
anon
Neil, good tip on using RAII to claim and free mutex. Thanks.
Ankur
I agree that the thread-unsafe underlying container functionality should exist as a basis first. But if I have an EquityTradeQueue, and a CurrencyTradeQueue, and a GovtBondTradeQueue, and a SandwichOrderQueue, and they all require exactly the same multi-threading semantics, then I'm going to write a ThreadSafeMessageQueue template for them to share. Also, I just remembered that the std::stack interface is impossible to "make thread-safe" anyway, because of the top()/pop() thing. What I said about offering stack's interface is wrong unless he basically just offers clients a pair<Locker,stack>.
Steve Jessop
... which is what you're advocating (minus wrapping them in a pair, which is pointless other than to pretend to fulfil the brief in the original interview question while really saying, "you shouldn't have asked me that question") ;-)
Steve Jessop
+1, I learnt something today :)
Nick D
"I'm going to write a ThreadSafeMessageQueue template for them to share". In C++, that is, since it's the best I can do. Better would be some sort of code weaving that allows the two concepts to be neatly combined, but in the absence of that, a semantics-adding wrapper is better IMO than a lot of duplicated code.
Steve Jessop
I'm really not clear if we are agreeing or disagreeing (a common problem when discussing via SO) but I would observe that EquityTradeQueue would not just be a queue and a lock - it would implement trading business logic too, and that is a my main point. A general, "thread safe queue" simply cannot be used in any given business situationm, whereas a basic (std or not) queue probably can. Note I've abandoned stack because I've never found a use for a thread-safe one (not to say such a use does not exist).
anon
Harper Shelby
@Harper I still think that's wrong. I may not be making myself clear here - I believe that multi-threading access is something that happens at the "business" level of objects, whereas choice of data structures happens at a much lower "programming" level. Notice that nobody here is suggesting using thread-safe strings, for example, because such things exist at far to low a level to be able to decide (or even be provided with) their own threading policy.
anon
Even worse, the algorithms used on strings make lots of accesses to them, and hence would not be thread safe even if the basic string ops were atomic. You'd have to specialise most of <algorithm>. I think queues and stacks are viable in that they (can) have such a small interface that it makes sense to define "all ops are atomic" versions of them, and that those versions will be useful. Just like atomic<int> is occasionally useful, even though you could say that multi-threading is a business-level concern, and clients should handle an int and a mutex themselves.
Steve Jessop
Of course it's possible that "thread-safe" is the wrong name / description for the class. QueueWithAtomicOps, perhaps, although that doesn't answer the question "atomic with respect to what?" (Other ops on the same structure? Other threads running at all? Signals? Interrupts?). My claim is that since atomic operations are a commonly-used building block of thread-safe designs, it makes sense to abstract there. As I say, this is a class which would be part of the application, not part of some library, so as far as I'm concerned it is part of the business level. But it's still a class.
Steve Jessop
+1  A: 

I would add a condition variable so that "poppers" can wait without burning CPU time.

Rhythmic Fistman
A: 

// tricky, returns a reference to the last element

The assignment copies the last element before it's popped off the vector, so that's fine.

As you say, "top" is pointless. You can grab the size of the vector any time you want it.

You should only ever call stack.empty() with the lock held, since there is no guarantee that it makes an atomic access. You could get an inconsistent answer if you call it while another thread is in the middle of updating the stack. So your public IsEmpty function should take the mutex, which means that you don't want to call it yourself from elsewhere.

But anyway, IsEmpty isn't very useful in parallel code. Just because it's false when you call it doesn't mean it will still be false one line later when you Pop. So either you should get rid of it from the public interface, or else you should expose the lock so that users can write their own atomic ops. In that case, I'd not have any underflow checking at all other than an assert in debug mode. But then, I've never believed in mollycoddling people who get as far as release mode without either reading the documentation or testing their code.

[Edit: How to use RAII for locks

When people say to use RAII for a lock, they don't just mean to make sure the mutex is destroyed. They mean use it to make sure the mutex is unlocked. The point is that if you have code which looks like this:

lock();
doSomething();
unlock();

and doSomething() throws an exception, then you won't unlock the mutex. Ouch.

So, here's an example class, along with usage:

class LockSession;
class Lock {
    friend class LockSession;
    public:
    Lock()        { pthread_mutex_init(&lock); }
    ~Lock()       { pthread_mutex_destroy(&lock); }

    private:
    void lock()   { pthread_mutex_lock(&lock); }
    void unlock() { pthread_mutex_unlock(&lock); }

    private:
    Lock(const Lock &);
    const Lock &operator=(const Lock &);

    private:
    pthread_mutex_t lock;
};

class LockSession {
    LockSession(Lock &l): lock(l) { lock.lock(); }
    ~LockSession()                { lock.unlock(); }
    private:
    LockSession(const LockSession &);
    LockSession &operator=(const LockSession &);

    private:
    Lock &lock;
};

Then somewhere your code will have a Lock associated with the data you want to protect, and will use it something like the following:

void doSomethingWithLock() {
    LockSession session(lock);
    doSomething();
}

or

void doSeveralThings() {
    int result = bigSlowComputation();  // no lock
    {
        LockSession s(lock);
        result = doSomething(result); // lock is held
    }
    doSomethingElse(result);     // no lock
}

Now it doesn't matter whether doSomething() throws an exception or returns normally (well, in the second example doSomethingElse won't happen on exception, but I'm assuming that's something that doesn't need to be done in an error situation). Either way, session is destroyed, and its destructor releases the mutex. In particular, operations like "push" on a stack allocate memory, and therefore might throw, and therefore you need to cope with that.

RAII stands for Resource Acquisition Is Initialization. In the case of doSomethingWithLock(), the resource you want to acquire is that you want to hold the lock. So you write a class which allows you to do that by initializing an object (the LockSession). When the object is destroyed, the lock is relinquished. So you're treating "locking/unlocking the mutex" exactly the same way you treat "initing/deiniting the mutex", and you protect yourself against resource leaks the same way.

One slightly annoying fact is that this code is completely broken and buggy, and you have to be sure not to accidentally do it, even though it looks to the careless eye just like the correct code:

void doSomethingWithLock() {
    LockSession(lock);
    doSomething();
}

Here the first line creates a temporary object and immediately destroys it, releasing the lock again. doSomething() is not called with the lock held.

Boost has a class template scoped_lock, which does what LockSession does, and more.]

Steve Jessop
Thanks for your comments.My concern is, since stack.back() would return a reference and that is assigned to the reference we have passed as parameter to PopElem routine, Isn't that reference becomes invalid when we do stack.pop_back() in the next statement. It would effectively be a dangling reference since the underlying element is deleted. I don't see any copying here. Am I missing something here.
Ankur
No, assigning a reference to a reference means "call the assignment operator to copy the contents of the referand on the rhs, to the referand on the lhs". It's copying, just as much as assigning to an object is copying. Remember that C++ references cannot be re-seated: when you assign anything to a reference (discounting assignment-style initialisation, which actually binds an object to the reference) you never affect what address the reference points to, you always copy (well, assign. But assignment usually means copying).
Steve Jessop
Btw, this is why in std::stack, they've separated top() (which returns a reference to the top element) from pop() (which removes the top element but doesn't let you see it). You're correct that without a copy you can't safely return an element and remove it, it's just that without you realising, your code as it stands does perform a copy...
Steve Jessop
A: 

I would throw away top at first. When you don't need it it is just waste!

Small is beautiful

Also if you wanted to optimize the accesses to vector: Duplicate handling of management information (here: stacklength) is always error prone. Better hope, that vector is brilliantly fast (STL most of the time is) and so empty() also is.

Juergen
A: 

This isn't idiomatic C++ and might not have any advantages but just for the novelty, have you considered implementing an immutable stack? That way, it would automatically be thread-safe.

Eric Lippert has done a C# implementation. Admittedly, the C++ code would be rather more involved.

Konrad Rudolph
A: 

One thing you didn't address is the issue of thread cancellation. The stl behaves badly when a thread is canceled during an operation on an stl container. You need to disable cancellation when you are operating on the vector. I found out the hard way about this. It is no fun when you have a deadlock and the threads are all in templated stl code and you are trying to debug exactly what happened. Use pthread_setcancelstate to change the cancellation state of the threads.

Chad Simpkins
+2  A: 

Neil, Onebyone:
An attempt on using RAII for mutex lock. Any comments?

template<typename T> 
class MyStack
{
public:
//interface
bool Push(T elem);
bool Pop(T& elem);
bool IsEmpty();

//constructor
MyStack() {
//top = 0;
}

//destructor
~MyStack() {

}

private:
    class Locker {   //RAII
    public:
     Locker() {
      pthread_mutex_init(&lock);
     }
     ~Locker() {
      pthread_mutex_destroy(&lock);
     }
     void Lock() {
      pthread_mutex_lock(&lock);
     }
     void UnLock() {
      pthread_mutex_unlock(&lock);
     }
    private:
     pthread_mutex_t lock;
    };
Locker MyLock;
//int top;
stack<T> mystack;

bool MyStack::Push(T elem);
bool MyStack::PushElem(T elem);
bool MyStack::Pop(T& elem);
bool MyStack::PopElem(T& elem);
}; //end of MyStack

template<typename T>
bool MyStack<T>::Push(T elem)
{
    MyLock.Lock();
    PushElem(elem);
    MyLock.UnLock();
}

template<typename T>
bool MyStack<T>::Pop(T& elem)
{
    MyLock.Lock();
    PopElem(elem);
    MyLock.UnLock();
}
Ankur
That's not what people mean when they say use RAII for the lock. I'll edit my answer to explain.
Steve Jessop