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

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线程指南(七)--资源竞争条件

时间: 2024-10-19 05:51:55

C++11线程指南(七)--资源竞争条件的相关文章

C++11线程指南(七)--死锁

1. 死锁 在多个mutex存在的时候,可能就会产生死锁. 避免死锁的一个最通用的方法是,总是按照相同的顺序来lock the two mutexes, 即总是先于mutex B之前lock mutex A,这样就不会有死锁的可能.有时,这种方法很简单实用,当这些mutexes用于不同的目标.但是,当mutexes用于包含相同类的一个实例时,就不是那么容易了. 例如,如下面程序所示,相同类的两个实例之间交互数据.为了保证数据交互不换并发影响,两个实例都使用mutex进行保护.但是当mutex被嵌

C++11线程指南(四)--右值引用与移动语义

1. 按值传递 什么是按值传递? 当一个函数通过值的方式获取它的参数时,就会包含一个拷贝的动作.编译器知道如何去进行拷贝.如果参数是自定义类型,则我们还需要提供拷贝构造函数,或者赋值运算符来进行深拷贝.然而,拷贝是需要代价的.在我们使用STL容器时,就存在大量的拷贝代价.当按值传递参数时,会产生临时对象,浪费宝贵的CPU以及内存资源. 需要找到一个减少不必要拷贝的方法.移动语义就是其中一种. 2. 右值引用 此处介绍右值引用的目的,是为了实现后面的移动语义. 右值引用使得我们可以分辨一个值是左值

C++11线程指南(二)--Lambda线程实现

1. Thread with lambda function 基于前一章中的Lambda程序,我们进行了扩展,当前创建5个线程. #include<iostream> #include<thread> #include<vector> #include<algorithm> int main() { std::vector<std::thread> threadVec; for(int i=0; i<5; ++i){ threadVec.p

C++11线程指南(五)--线程的移动语义实现

1. 线程的移动语义实现 基于前面几章介绍的移动语义,我们用它来实现线程. #include <iostream> #include <thread> #include <vector> #include <algorithm> #include <cassert> int main() { std::vector<std::thread> workers; for (int i = 0; i < 5; i++) { auto

C++11 并发指南系列(转)

本系列文章主要介绍 C++11 并发编程,计划分为 9 章介绍 C++11 的并发和多线程编程,分别如下: C++11 并发指南一(C++11 多线程初探)(本章计划 1-2 篇,已完成 1 篇) C++11 并发指南二(std::thread 详解)(本章计划 1-2 篇,已完成 1 篇) C++11 并发指南三(std::mutex 详解)(本章计划 1-2 篇,已完成 2 篇) C++11 并发指南三(std::mutex 详解) C++11 并发指南三(Lock 详解) C++11 并发

好记性不如烂笔头75-多线程-并行访问下的资源竞争和样例

在实际业务常见中,很多的时候我们都需要访问一些共同的资源,比如一个序列号,比如某一个文件.如果多个线程一起访问这个序列或者文件,而我们没有做足够多的处理,就很容易造成脏数据或者数据丢失等各种问题. 这种场景特别常见,写一个简单的例子.以免自己的团队在实际开发中,犯这种小错误. 当然,这种错误知道了,要预先处理还是相当简单:但是如果要真的出现了错误,在一大堆代码中找这个坑,那是相当的要命. 样例的场景 我们的业务需要获取一个序列号.下面是一个取得序列号的单例模式的例子,但调用get()时,可能会产

java线程共享受限资源 解决资源竞争 thinking in java4 21.3

java线程共享受限资源 解决资源竞争  详细介绍请参阅:thinking in java4 21.3 thinking in java 4免费下载:http://download.csdn.net/detail/liangrui1988/7580155 package org.rui.thread.res; /** * 不正确的访问 资源 * @author lenovo * */ public abstract class IntGenerator { private volatile bo

c++11线程之条件变量condition_variable(二)

题目:编写一个程序,开启3个线程,这3个线程的ID分别为A.B.C,每个线程将自己的ID在屏幕上打印10遍,要求输出结果必须按ABC的顺序显示:如:ABCABC-.依次递推. 采用C++11实现: [cpp] view plaincopyprint? #include<iostream> #include<thread> #include<mutex> #include<condition_variable> using namespace std; mut

c++11线程之条件变量condition_variable

题目:子线程循环 10 次,接着主线程循环 100 次,接着又回到子线程循环 10 次,接着再回到主线程又循环 100 次,如此循环50次,试写出代码. [cpp] view plaincopyprint? #include<iostream> #include<thread> #include<mutex> #include<condition_variable> using namespace std; mutex m; condition_variab