在最近的一次采访中,我被要求在Linux机器上用C ++实现一个线程安全的通用(基于ietemplate)堆栈。
我很快想出了以下内容(它可能有编译错误)
我完成了。面试官可能喜欢这个实现中的一些东西。也许设计部分:)
以下是此实施可能存在的一些问题: -
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();
}
答案 0 :(得分:8)
一些问题:
答案 1 :(得分:2)
Neil,Onebyone:
尝试使用RAII进行互斥锁定。任何意见?
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();
}
答案 2 :(得分:1)
我会添加一个条件变量,以便“poppers”可以等待而不会占用CPU时间。
答案 3 :(得分:1)
//棘手,返回对最后一个元素的引用
该赋值在向量弹出向量之前复制最后一个元素,所以没关系。
正如你所说,“顶级”毫无意义。您可以随时获取矢量的大小。
你应该只在持有锁的情况下调用stack.empty(),因为不能保证它会进行原子访问。如果在另一个线程正在更新堆栈的过程中调用它,则可能会得到一个不一致的答案。因此,您的公共IsEmpty函数应该使用互斥锁,这意味着您不希望自己从其他地方调用它。
但无论如何,IsEmpty在并行代码中并不是很有用。只是因为当你调用它时它是错误的并不意味着当你弹出它时它仍会是假的。所以要么你应该从公共接口中删除它,否则你应该公开锁,以便用户可以编写自己的原子操作。在这种情况下,除了调试模式下的断言之外,我根本没有任何下溢检查。但是,我从来没有相信过那些在没有阅读文档或测试代码的情况下获得发布模式的人。
[编辑:如何使用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
都被销毁,其析构函数释放互斥锁。特别是,堆栈上的“push”操作会分配内存,因此可能会抛出,因此您需要处理它。
RAII代表资源获取是初始化。在doSomethingWithLock()的情况下,您要获取的资源是您想要持有锁。所以你编写了一个类,它允许你通过初始化一个对象(LockSession)来做到这一点。当对象被销毁时,锁被放弃。因此,您正在处理“锁定/解锁互斥锁”与处理“启动/删除互斥锁”的方式完全相同,并以同样的方式保护自己免受资源泄漏。
一个有点令人烦恼的事实是,这段代码完全被破坏了,并且你必须确保不要意外地做到这一点,即使它看起来像粗心的眼睛一样:
void doSomethingWithLock() {
LockSession(lock);
doSomething();
}
这里第一行创建一个临时对象并立即销毁它,再次释放锁。保持锁定不会调用doSomething()
。
Boost有一个类模板scoped_lock
,它可以执行LockSession所做的事情,等等。]
答案 4 :(得分:0)
我会先丢掉顶部。当你不需要它时,它只是浪费!
小而美丽
此外,如果您想优化对vector的访问:管理信息的重复处理(此处:stacklength)总是容易出错。更好的希望,那个矢量非常快(STL大部分时间都是这样),所以也是空的()。
答案 5 :(得分:0)
这不是惯用的C ++,可能没有任何优势,只是为了新颖性,您是否考虑过实现不可变堆栈?这样,它将自动成为线程安全的。
Eric Lippert做了C# implementation。不可否认,C ++代码将更加复杂。
答案 6 :(得分:0)
您没有解决的一件事是线程取消问题。在对stl容器执行操作期间取消线程时,stl表现不佳。在向量上操作时,需要禁用取消。我发现了很难的方法。当你遇到死锁并且线程都是模板化的stl代码并且你正在尝试调试发生的事情时,这并不好玩。使用pthread_setcancelstate更改线程的取消状态。