1. 接口设计1
下面例子使用vector实现了一个栈。两个线程轮流从中弹出元素。
#include <iostream> #include <thread> #include <mutex> #include <string> #include <vector> std::mutex myMutex; class Stack { public: Stack() {}; ~Stack() {}; void pop(); int top() { return data.back(); } void push(int); void print(); int getSize() { return data.size(); } private: std::vector<int> data; }; void Stack::pop() { std::lock_guard<std::mutex> guard(myMutex); data.erase(data.end()-1); } void Stack::push(int n) { std::lock_guard<std::mutex> guard(myMutex); data.push_back(n); } void Stack::print() { std::cout << "initial Stack : " ; for(int item : data) std::cout << item << " "; std::cout << std::endl; } void process(int val, std::string s) { std::lock_guard<std::mutex> guard(myMutex); std::cout << s << " : " << val << std::endl; } void thread_function(Stack& st, std::string s) { int val = st.top(); st.pop(); process(val, s); } int main() { Stack st; for (int i = 0; i < 10; i++) st.push(i); st.print(); while(true) { if(st.getSize() > 0) { std::thread t1(&thread_function, std::ref(st), std::string("thread1")); t1.join(); } else break; if(st.getSize() > 0) { std::thread t2(&thread_function, std::ref(st), std::string("thread2")); t2.join(); } else break; } return 0; }
运行后的结果之一:
initial Stack : 0 1 2 3 4 5 6 7 8 9
thread1 : 9
thread2 : 8
thread1 : 7
thread2 : 6
thread1 : 5
thread2 : 4
thread1 : 3
thread2 : 2
thread1 : 1
thread2 : 0
看上去这段代码是线程安全的。事实上并非如此。仍然有资源竞争存在,取决于执行的顺序。如下所示:
元素"6"可能被执行两次,且元素"5"被跳过了。
尽管从上面的运行结果看是正确的,但是代码中仍然存在可能触发资源竞争的条件。换言之,这段代码不是线程安全的。
一种解决方法是将函数top()与pop()合并到一个mutex下面:
int stack::pop() { lock_guard<mutex> guard(myMutex); int val = data.back(); data.erase(data.end()-1); return val; } void thread_function(stack& st, string s) { int val = st.pop(); process(val, s); }
2. 接口设计2
假设需要处理一个双向链表list.
为了保证一个线程可以安全的从双向链表中删除一个node, 我们需要同时保证3个node的并发操作正确。即要删除的node,以及它前后的两个node. 如果我们对每个node都单独的进行保护,这跟不使用mutex没什么区别,因为竞争还是会发生。需要被包含的不是每个step中的单个node, 而是整个的删除操作。最简便的方法就是使用mutex保护整个list。
仅仅依靠单独的操作list来实现线程安全,我们还是没有达到目的。仍然可能存在竞争,即使使用的是一个很简单的接口。例如,下面的std::stack container adapter构成的栈数据结构。除了构造函数与swap()之外,还有5个函数是需要实现的。
push() - 插入新的元素入栈
pop() - 元素退栈
top() - 获取栈顶元素
empty() - 检测栈是否为空
size() - 元素个数
如果对top()函数进行修改,使得它返回的是一个拷贝,而不是引用,并且内部使用mutex来保护,这个接口仍然会存在竞争条件。问题不在于基于mutex来实现,而是这是一个接口问题,如果stack实现的是lock-free,则问题仍然存在。
#include <mutex> #include <deque> using namespace std; template< typename T, typename Container = std::deque<T> > class stack { public: explicit stack(const Container&); explicit stack(Container&& = Container()); template <typename Alloc> explicit stack(const Alloc&); template <typename Alloc> stack(const Container&, const Alloc&); template <typename Alloc> stack(Container&&, const Alloc&); template <typename Alloc> stack(stack&&, const Alloc&); // not reliable bool empty() const; // not reliable size_t size() const; T& top(); T const& top() const; void push(T const&); void push(T&&); void pop(); void swap(stack&&); };
此段代码的问题在与empty()与size()是不可靠的。在某个线程调用empty或size之前,其它线程可能已经调用了push或pop对栈进行了改变。
特别地,当一个stack instance不共享时,可以安全的调用empty()以及top(),如下:
stack<int> s; if(!s.empty()) { int const value=s.top(); s.pop(); do_task(value); }
不过,当stack instance共享时,对于一个空栈,调用top()可能导致未知的结果。调用顺序empty() -- > top() -- >pop()不再线程安全。empty()与top()之间,可能有另一个线程调用过了pop().
因此,这是一个接口调用顺序导致的竞争问题,而不是因为没有对底层资源进行保护产生的。那么,怎么解决呢?
因为这是接口顺序导致的,因此方法就是修改这个接口。
最简单的方法,top()抛出一个异常,如果栈是空的。但是这会增加程序复杂性。
C++11线程指南(七)--资源竞争条件