线程安全向量:这个实现是否是线程安全的?

时间:2016-09-09 17:38:03

标签: c++ multithreading thread-safety

我对线程安全这个术语有疑问。让我举个例子:

#include <mutex>
#include <vector>

/// A thread-safe vector
class ThreadSafeVector {
private:
  std::mutex m;
  std::vector<double> v;

public:
  // add double to vector
  void add(double d) {
    std::lock_guard<std::mutex> lg(m);
    v.emplace_back(d);
  }

  // return length of vector  
  int length() {
    std::lock_guard<std::mutex> lg(m);
    return v.size();
  }   
};

你会调用那个类,即它的所有方法,线程安全吗?

编辑[周日晚上9点CEST]

在得到一些好的“是的,但是”-answers和替代实现之后,我在下面的答案中提供了我自己的观点。基本上,它归结为一个简单的问题,类的线程安全性是否只需要为PARALLEL执行其方法提供强大的原子性和可见性保证,或者类是否必须提供超出其自身范围的保证(例如SERIAL)执行)。

7 个答案:

答案 0 :(得分:4)

IMHO:

这既安全又有用:

TextView.LineSpacing

这是安全但无用的:

  void add(double d) {
    std::lock_guard<std::mutex> lg(m);
    v.emplace_back(d);
  }

因为当你获得自己的长度时,它可能已经发生了变化,所以推理它不太可能有用。

这个怎么样?

  // return length of vector  
  int length() {
    std::lock_guard<std::mutex> lg(m);
    return v.size();
  }   

这样叫:

template<class Func>
decltype(auto) do_safely(Func&& f)
{
  std::lock_guard<std::mutex> lock(m);
  return f(v);
}

答案 1 :(得分:2)

您提供的是线程安全的。但是,问题在于您无法添加允许访问元素而不会丢失线程安全性的方法。此类线程安全性也非常低效。有时你想迭代整个容器,有时你想要一个接一个地添加许多元素。

作为替代方案,您可以负责锁定呼叫者。这样效率更高。

/// A lockable vector
class LockableVector
{
public:
    using mutex_type = std::shared_timed_mutex;
    using read_lock = std::shared_lock<mutex_type>;
    using write_lock = std::unique_lock<mutex_type>;

    // default ctor
    LockableVector() {}

    // move-ctor
    LockableVector(LockableVector&& lv)
    : LockableVector(std::move(lv), lv.lock_for_writing()) {}

    // move-assign
    LockableVector& operator=(LockableVector&& lv)
    {
        lv.lock_for_writing();
        v = std::move(lv.v);
        return *this;
    }

    read_lock lock_for_reading() { return read_lock(m); }
    write_lock lock_for_writing() { return write_lock(m); }

    // add double to vector
    void add(double d) {
        v.emplace_back(d);
    }

    // return length of vector
    int length() {
        return v.size();
    }

    // iteration
    auto begin() { return v.begin(); }
    auto end() { return v.end(); }

private:
    // hidden, locked move-ctor
    LockableVector(LockableVector&& lv, write_lock)
    : v(std::move(lv.v)) {}

    mutex_type m;
    std::vector<double> v;
};

int main()
{
    LockableVector v;

    // add lots of elements

    { /// create a scope for the lock
        auto lock = v.lock_for_writing();

        for(int i = 0; i < 10; ++i)
            v.add(i);
    }

    // print elements

    { /// create a scope for the lock
        auto lock = v.lock_for_reading();

        std::cout << std::fixed << std::setprecision(3);
        for(auto d: v)
            std::cout << d << '\n';
    }
}

同时通过同时具有读锁定和写锁定可以提高效率,因为当没有线程当前正在写入时,您可以同时拥有多个读取器。

答案 2 :(得分:1)

虽然这是线程安全的,但效率不高。通过使用shared_mutex(C ++ 14或Boost,它不在C ++ 11中),您可以轻松地提高效率。这是因为如果两个线程要求大小,这应该不是问题。但是,如果一个线程要求大小而另一个想要添加一个元素,那么只允许其中一个进行访问。

所以我会改变你的代码:

#include <mutex>
#include <vector>
#include <shared_mutex>

/// A thread-safe vector
class ThreadSafeVector {
private:
  mutable std::shared_timed_mutex m; //notice the mutable
  std::vector<double> v;

public:
  // add double to vector
  void add(double d) {
    std::unique_lock<std::shared_timed_mutex> lg(m); //just shared_mutex doesn't exist in C++14, you're welcome to use boost::shared_mutex, it's the same
    v.emplace_back(d);
  }

  // return length of vector  
  //notice the const, because this function is not supposed to modify your class
  int length() const {
    std::shared_lock<std::shared_timed_mutex> lg(m);
    return v.size();
  }   
};

要记住的一些事项:

  • std::mutex(以及所有其他互斥锁)都是不可复制的。这意味着您的班级现在不可复制。要使其可复制,您必须自己实现复制构造函数并绕过复制互斥锁。

  • 始终在容器中创建互斥锁mutable。这是因为修改互斥锁并不意味着您正在修改类的内容,这与我添加到const方法的length()兼容。 const表示此方法不会修改类中的任何内容。这是一个很好的做法。

答案 3 :(得分:1)

虽然你的矢量一旦开始使用就看起来像线程安全,你会发现它不是。例如,如果向量小于5(保持不大于5),我想向向量添加任务

ThreadSafeVector tv;

if( tv.length() < 5 ) tv.add( 10.0 );

这会在多线程环境中正常工作吗?不会。随着您向矢量添加更多逻辑,这将变得越来越复杂。

答案 4 :(得分:0)

是的,我愿意。两个公共方法都受锁保护,并且隐式删除所有特殊成员函数(复制/移动构造函数/赋值),因为while(temp1 != NULL) // <-- this line was wrong { temp2=temp1; if(value<=temp1->data) { temp1=temp1->lchild; } else { temp1=temp1->rchild; } } 既不可复制也不可移动。

答案 5 :(得分:0)

我们正在进行内部团队讨论线程安全的含义。 Slavas评论“尽管长度()在技术上是”线程安全的“,但实际上并非”将其归结为矛盾的本质。也许用“是”或“否”回答我的简单问题是不可能的?

在这里,我的观点是:线程安全只需要关于PARALLEL执行其操作的干净语义。 ThreadSafeVector类是线程安全的,因为它的函数保证了以下操作的并行执行:

  • 原子性:交错线程不会导致状态不一致(因为锁定)
  • 可见性:状态被传播以供其他线程看到(因为互斥锁定引起的内存障碍)

调用类线程安全不要求它的任何可能的聚合使用必须是自己的线程安全的,即类的方法的串行执行不必是线程安全的。例如:

if (a.length() == 0) a.add(42);

当然这行不是线程安全的,因为它本身不是原子的,并且类甚至不提供“工具”来做这样的事情。但仅仅因为我可以从线程安全操作构造一个非线程安全的序列,并不意味着线程安全操作实际上不是线程安全的。

答案 6 :(得分:0)

将线程安全性包装在传统上线程不安全类周围的最好,最无错误的方法之一是使用监视器:

template<class T>
class monitor
{
public:
    template<typename ...Args>
    monitor(Args&&... args) : m_cl(std::forward<Args>(args)...){}

    struct monitor_helper
    {
        monitor_helper(monitor* mon) : m_mon(mon), m_ul(mon->m_lock) {}
        T* operator->() { return &m_mon->m_cl;}
        monitor* m_mon;
        std::unique_lock<std::mutex> m_ul;
    };

    monitor_helper operator->() { return monitor_helper(this); }
    monitor_helper ManuallyLock() { return monitor_helper(this); }
    T& GetThreadUnsafeAccess() { return m_cl; }

private:
    T           m_cl;
    std::mutex  m_lock;
};

这将使您以线程安全的方式访问包装类的所有方法:

monitor<std::vector<int>> threadSafeVector {5};

然后使用

threadSafeVector->push_back(5);

或任何其他成员函数可在锁定状态下执行调用。有关更多信息,请参见我的原始答案here

这不会神奇地在逻辑上安全地进行多个调用(如其他答案所述),但是该系统也可以实现这一目的:

// You can explicitly take a lock then call multiple functions
// without the overhead of a relock each time. The 'lock handle'
// destructor will unlock the lock correctly. This is necessary
// if you want a chain of logically connected operations 
{
    auto lockedHandle = threadSafeVector.ManuallyLock();
    if(!lockedHandle->empty())
    {
        lockedHandle->pop_back();
        lockedHandle->push_back(-3);
    }
}