[英]Implementing a thread-safe, generic stack in C++ on linux
在最近的一次采訪中,我被要求在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();
}
一些問題:
尼爾,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();
}
我會添加一個條件變量,以便“poppers”可以等待而不會占用CPU時間。
//棘手,返回對最后一個元素的引用
賦值在向量彈出之前復制最后一個元素,這樣就可以了。
正如你所說,“頂級”毫無意義。 您可以隨時獲取矢量的大小。
您應該只使用鎖定調用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;
};
然后在某處你的代碼將有一個與你想要保護的數據相關聯的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所做的事情,以及更多。]
我會先扔掉頂部。 當你不需要它時,它只是浪費!
小是美麗的
此外,如果您想優化對向量的訪問:管理信息的重復處理(此處:stacklength)總是容易出錯。 更好的希望,那個矢量非常快(STL大部分時間都是),所以空()也是。
這不是慣用的C ++,可能沒有任何優勢,但僅僅是為了新穎性,您是否考慮過實現不可變堆棧? 這樣,它將自動成為線程安全的。
Eric Lippert完成了C#實現 。 不可否認,C ++代碼將更加復雜。
你沒有解決的一件事是線程取消的問題。 在對stl容器執行操作期間取消線程時,stl表現不佳。 在向量上操作時,需要禁用取消。 我發現了很難的方法。 當你遇到死鎖並且線程都是模板化的stl代碼並且你正在嘗試調試發生的事情時,這並不好玩。 使用pthread_setcancelstate更改線程的取消狀態。
聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.