【问题标题】:Implementing a thread-safe, generic stack in C++ on linux在 Linux 上用 C++ 实现线程安全的通用堆栈
【发布时间】:2009-06-26 12:12:49
【问题描述】:

在最近的一次采访中,我被要求在 Linux 机器上用 C++ 实现一个线程安全的通用(即基于模板)堆栈。
我很快想出了以下(可能有编译错误)。
我通过了。面试官可能喜欢这个实现中的一些东西。也许是设计部分:)
以下是此实现可能存在的一些问题:-
1. 指示溢出/下溢的错误实现。由于我使用 STL 向量作为底层数据结构,因此没有溢出处理。应该有这样的处理吗?此外,下溢(在 Pop() 中)产生 false 作为返回值。是否应该通过抛出异常来完成?
2. PopElem 例程的实现。下面的实现是否正确?
3.没有真正使用顶部元素。
4. 编写器和读取器线程之间的更好时机。

请提出任何建议/建议/改进。
谢谢。

//实现一个线程安全的泛型栈。

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

【问题讨论】:

  • 为什么 Pop() 带参数? pop 是否应该移除栈顶元素?
  • @Appu Pop 接受一个参数,以便您可以引用从堆栈中删除的项目。

标签: c++ multithreading stl


【解决方案1】:

一些问题:

  • 我将实现一个 Locker 类来使用 RAII 声明和释放互斥锁
  • 我会使用 std::stack
  • 我会让 std::stack 的用户使用 Locker 来实现锁定策略 - 拥有一个锁定自身的堆栈是不好的设计,因为堆栈不知道如何使用它

【讨论】:

  • “拥有一个自锁的堆栈是糟糕的设计”。你怎么知道的?如果堆栈的用户发现他总是使 push 和 pop 原子化,并且不需要其他原子操作,为什么封装这个需求是“糟糕的设计”?此规则是否适用于堆栈或其他集合?拥有一个自锁的消息队列是一个糟糕的设计,还是应该禁止消息队列的用户封装他们的线程安全?这个禁令有多高?等等等等。不过,您绝对正确,他应该只包装 std::stack 并复制其接口。
  • 您需要区分对象的容器行为及其多线程访问模式。基本上可以推送和弹出堆栈,队列可以添加和删除内容。这些都与多线程无关。现在交易系统中的 EquityTradeQueue 类将同时具有队列行为和多线程行为,但最好使用单独的组件而不是某种“线程安全队列”来实现这些行为。这是基于相当多的个人经验,顺便说一句>
  • Neil,关于使用 RAII 声明和释放互斥锁的好技巧。谢谢。
  • 我同意线程不安全的底层容器功能应该首先作为基础存在。但是如果我有一个 EquityTradeQueue、一个 CurrencyTradeQueue、一个 GovtBondTradeQueue 和一个 SandwichOrderQueue,并且它们都需要完全相同的多线程语义,那么我将编写一个 ThreadSafeMessageQueue 模板供它们共享。另外,我只记得 std::stack 接口无论如何都不可能“使线程安全”,因为 top()/pop() 的事情。我所说的提供堆栈的接口是错误的,除非他基本上只是为客户提供一对
  • @Harper 我仍然认为这是错误的。我在这里可能没有说清楚——我相信多线程访问发生在对象的“业务”级别,而数据结构的选择发生在低得多的“编程”级别。请注意,这里没有人建议使用线程安全的字符串,例如,因为这样的东西存在于非常低的级别,以便能够决定(甚至提供)他们自己的线程策略。
【解决方案2】:

尼尔,Onebyone:
尝试使用 RAII 进行互斥锁。有cmets吗?

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

【讨论】:

  • 这不是人们所说的使用 RAII 作为锁的意思。我会编辑我的答案来解释。
【解决方案3】:

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

【讨论】:

    【解决方案4】:

    // 棘手,返回对最后一个元素的引用

    赋值复制了从向量中弹出之前的最后一个元素,所以这很好。

    正如您所说,“顶部”毫无意义。您可以随时获取矢量的大小。

    您应该只在持有锁的情况下调用 stack.empty(),因为不能保证它会进行原子访问。如果在另一个线程正在更新堆栈的过程中调用它,您可能会得到不一致的答案。所以你的公共 IsEmpty 函数应该使用互斥体,这意味着你不想从其他地方自己调用它。

    但无论如何,IsEmpty 在并行代码中并不是很有用。仅仅因为当你调用它时它是错误的,并不意味着当你 Pop 之后它仍然是错误的一行。所以要么你应该从公共接口中去掉它,要么你应该公开锁,以便用户可以编写他们自己的原子操作。在这种情况下,除了调试模式下的断言之外,我根本没有任何下溢检查。但是,我从不相信在没有阅读文档或测试他们的代码的情况下就进入发布模式的人溺爱。

    [编辑:如何将 RAII 用于锁

    当人们说要使用 RAII 来锁定时,他们不仅仅意味着要确保互斥体被销毁。他们的意思是使用它来确保互斥锁已解锁。关键是,如果您的代码如下所示:

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

    并且 doSomething() 抛出异常,那么您将无法解锁互斥锁。哎哟。

    所以,这是一个示例类,以及用法:

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

    然后,您的代码将在某个地方有一个与您要保护的数据相关联的锁,并将使用它,如下所示:

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

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

    现在doSomething() 抛出异常或正常返回都没有关系(好吧,在第二个示例中,doSomethingElse 不会发生异常,但我假设这是不需要的在错误情况下完成)。无论哪种方式,session 都会被销毁,并且其析构函数会释放互斥锁。特别是,在堆栈上“推送”之类的操作会分配内存,因此可能会抛出异常,因此您需要处理它。

    RAII 代表资源获取即初始化。在 doSomethingWithLock() 的情况下,您要获取的资源是您要持有锁。所以你编写了一个类,它允许你通过初始化一个对象(LockSession)来做到这一点。当对象被销毁时,锁被放弃。因此,您对待“锁定/解锁互斥锁”的方式与对待“初始化/取消初始化互斥锁”的方式完全相同,并且您以同样的方式保护自己免受资源泄漏。

    一个有点烦人的事实是这段代码完全被破坏和错误,你必须确保不要意外地这样做,即使它看起来像正确的代码一样粗心:

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

    这里第一行创建了一个临时对象并立即销毁它,再次释放锁。 doSomething() 不会在持有锁的情况下被调用。

    Boost 有一个类模板 scoped_lock,它执行 LockSession 的功能等等。]

    【讨论】:

    • 感谢您的 cmets。我担心的是,由于 stack.back() 会返回一个引用,并且该引用被分配给我们作为参数传递给 PopElem 例程的引用,当我们在下一个语句中执行 stack.pop_back() 时,该引用是否会变得无效。由于删除了底层元素,因此它实际上是一个悬空引用。我在这里看不到任何抄袭。我在这里错过了什么吗?
    • 不,将引用分配给引用意味着“调用赋值运算符将rhs上的referand内容复制到lhs上的referand”。它是复制,就像分配给一个对象是复制一样。请记住,C++ 引用不能被重新定位:当您将任何内容分配给引用时(折扣式初始化,它实际上将对象绑定到引用)您永远不会影响引用指向的地址,您总是复制(好吧,分配. 但是赋值通常意味着复制)。
    • 顺便说一句,这就是为什么在 std::stack 中,他们将 top() (返回对顶部元素的引用)与 pop() (删除顶部元素但不让你看看)。你是对的,没有副本你不能安全地返回一个元素并删除它,只是在你没有意识到的情况下,你的代码确实执行了一个副本......
    【解决方案5】:

    一开始我会扔掉上衣。当你不需要它时,它只是浪费!

    小而美

    另外,如果您想优化对向量的访问:重复处理管理信息(此处为:stacklength)总是容易出错。更好的希望是,这个向量非常快(大多数时候是 STL),所以 empty() 也是。

    【讨论】:

      【解决方案6】:

      这不是惯用的 C++,可能没有任何优势,但只是为了新颖,您是否考虑过实现不可变堆栈?这样一来,它就会自动成为线程安全的。

      Eric Lippert 完成了C# implementation。诚然,C++ 代码会涉及更多。

      【讨论】:

        【解决方案7】:

        您没有解决的一件事是线程取消问题。当在 stl 容器上的操作期间取消线程时,stl 的行为很糟糕。对向量进行操作时需要禁用取消。我发现了这个困难的方法。当您遇到死锁并且线程都在模板化的 stl 代码中并且您正试图准确调试发生的事情时,这并不好玩。使用 pthread_setcancelstate 改变线程的取消状态。

        【讨论】:

          猜你喜欢
          • 1970-01-01
          • 2010-10-21
          • 1970-01-01
          • 2020-08-20
          • 2017-09-03
          • 1970-01-01
          • 2011-05-08
          • 2017-07-27
          • 1970-01-01
          相关资源
          最近更新 更多