C++11线程指南(7)--资源竞争条件

目录

1.场景一(范围)

2.场景二(顺序)


本文通过2个例子,来对多线程的资源竞争进行说明。

第1个是互斥范围。使用mutex等进行保护时,需要仔细选择保护的域的范围。范围选择太小,可能起不到线程互斥的作用。而范围太大,可能影响程序的执行效率和性能。

第2个是调用顺序。在一些场景下,即使采用了互斥机制,也不一定能确保线程安全,因为还需要考虑接口的调用顺序。

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.场景二(顺序)

  假设需要处理一个双向链表list.
  为了保证一个线程可以安全的从双向链表中删除一个node, 我们需要同时保证3个node的并发操作正确。即要删除的node,以及它前后的两个node. 如果我们对每个node都单独的进行保护,这跟不使用mutex没什么区别,因为竞争还是会发生。
  需要被保护的不是每个step中的单个node, 而是整个的删除操作。最简便的方法就是使用mutex保护整个list。
  仅仅依靠单独的操作list来实现线程安全,我们还是没有达到目的,仍然可能存在竞争,即使使用的是一个很简单的接口。
  例如,下面的std::stack container构成的栈数据结构。除了构造函数与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()抛出一个异常。但是这会增加程序复杂性。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值