简体   繁体   English

在Linux上用C ++实现一个线程安全的通用堆栈

[英]Implementing a thread-safe, generic stack in C++ on linux

In a recent interview, I was asked to implement a thread safe generic (ietemplate based) stack in C++, on linux machine. 在最近的一次采访中,我被要求在Linux机器上用C ++实现一个线程安全的通用(基于ietemplate)堆栈。
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. 1.表示溢出/下溢的实现不正确。 There is no overflow handling since I'm using STL vector as the underlying data structure. 因为我使用STL向量作为底层数据结构,所以没有溢出处理。 Should there be any such handling? 应该有这样的处理吗? Also, underflow (in Pop()) yields false as return value. 此外,下溢(在Pop()中)产生false作为返回值。 Should it be done by throwing of an exception? 应该通过抛出异常来完成吗?
2. Implementation of PopElem routine. 2. PopElem例程的实现。 Is the below implementation correct? 以下实施是否正确?
3. No real use of top element. 3.没有真正使用顶级元素。
4. Better timing between start of writer and reader thread. 4.编写器和读者线程启动之间的更好时间。

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();
}

Some issues: 一些问题:

  • I would implement a Locker class to claim & free the mutex using RAII 我将实现一个Locker类来声明并使用RAII释放互斥锁
  • I would use std::stack 我会使用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 我会让std :: stack的用户使用Locker来实现锁定策略 - 拥有一个锁定自身的堆栈是糟糕的设计,因为堆栈无法知道它是如何被使用的

Neil, Onebyone: 尼尔,Onebyone:
An attempt on using RAII for mutex lock. 尝试使用RAII进行互斥锁定。 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();
}

我会添加一个条件变量,以便“poppers”可以等待而不会占用CPU时间。

// 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. 您应该只使用锁定调用stack.empty(),因为无法保证它进行原子访问。 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. 因此,您的公共IsEmpty函数应该使用互斥锁,这意味着您不希望自己从其他地方调用它。

But anyway, IsEmpty isn't very useful in parallel code. 但无论如何,IsEmpty在并行代码中并不是很有用。 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 [编辑:如何使用RAII进行锁定

When people say to use RAII for a lock, they don't just mean to make sure the mutex is destroyed. 当人们说使用RAII进行锁定时,他们并不仅仅意味着确保互斥锁被破坏。 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. 并且doSomething()抛出异常,然后您将无法解锁互斥锁。 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: 然后在某处你的代码将有一个与你想要保护的数据相关联的Lock,并将使用类似如下的代码:

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). 现在无论doSomething()抛出异常还是正常返回都没关系(好吧,在第二个例子中, doSomethingElse不会在异常时发生,但我假设在错误的情况下不需要这样做)。 Either way, session is destroyed, and its destructor releases the mutex. 无论哪种方式, session被销毁,其析构函数释放互斥锁。 In particular, operations like "push" on a stack allocate memory, and therefore might throw, and therefore you need to cope with that. 特别是,堆栈上的“push”操作会分配内存,因此可能会抛出,因此您需要应对这种情况。

RAII stands for Resource Acquisition Is Initialization. RAII代表资源获取是初始化。 In the case of doSomethingWithLock(), the resource you want to acquire is that you want to hold the lock. 在doSomethingWithLock()的情况下,您要获取的资源是您想要持有锁。 So you write a class which allows you to do that by initializing an object (the LockSession). 所以你编写了一个类,它允许你通过初始化一个对象(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. 锁定时不会调用doSomething()

Boost has a class template scoped_lock , which does what LockSession does, and more.] Boost有一个类模板scoped_lock ,它执行LockSession所做的事情,以及更多。]

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. 此外,如果您想优化对向量的访问:管理信息的重复处理(此处:stacklength)总是容易出错。 Better hope, that vector is brilliantly fast (STL most of the time is) and so empty() also is. 更好的希望,那个矢量非常快(STL大部分时间都是),所以空()也是。

This isn't idiomatic C++ and might not have any advantages but just for the novelty, have you considered implementing an immutable stack? 这不是惯用的C ++,可能没有任何优势,但仅仅是为了新颖性,您是否考虑过实现不可变堆栈? That way, it would automatically be thread-safe. 这样,它将自动成为线程安全的。

Eric Lippert has done a C# implementation . Eric Lippert完成了C#实现 Admittedly, the C++ code would be rather more involved. 不可否认,C ++代码将更加复杂。

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. 在对stl容器执行操作期间取消线程时,stl表现不佳。 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. 当你遇到死锁并且线程都是模板化的stl代码并且你正在尝试调试发生的事情时,这并不好玩。 Use pthread_setcancelstate to change the cancellation state of the threads. 使用pthread_setcancelstate更改线程的取消状态。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM