【问题标题】:Thread synchronization between data pointed by vectors of std::shared_ptrstd::shared_ptr 的向量指向的数据之间的线程同步
【发布时间】:2017-11-30 15:21:41
【问题描述】:

我对并发编程很陌生,我有一个特定的问题,我无法通过浏览互联网找到解决方案。..

基本上我有这种情况(示意图伪代码):

void fun1(std::vector<std::shared_ptr<SmallObj>>& v) {
  for(int i=0; i<v.size(); i++)
    .. read and write on *v[i] ..
}

void fun2(std::vector<std::shared_ptr<SmallObj>>& w) {
  for(int i=0; i<w.size(); i++)
    .. just read on *w[i] ..
}


int main() {

 std::vector<std::shared_ptr<SmallObj>> tot;

 for(int iter=0; iter<iterMax; iter++) {
   for(int nObj=0; nObj<nObjMax; nObj++)
      .. create a SmallObj in the heap and store a shared_ptr in tot ..

   std::vector<std::shared_ptr<SmallObj>> v, w;
   .. copy elements of "tot" in v and w ..

   fun1(v);
   fun2(w);
 }

 return 0;
}

我想要做的是同时操作产生两个线程来执行 fun1 和 fun2 但我需要使用一些锁定机制来规范对 SmallObj 的访问。我该怎么做?在文献中,我只能找到使用互斥锁来锁定对特定对象或部分代码的访问的示例,但不能找到不同对象(在本例中为 v 和 w)对相同指向变量的访问。

非常感谢您,并为我对此事的无知感到抱歉..

【问题讨论】:

  • 只需将修改代码包装在互斥体中即可。这样一次只有一个线程修改。
  • 没有法律要求给定的互斥锁只能用于锁定单个对象。您可以创建一个互斥锁,并在您想要访问与共享指针相关的任何内容时锁定同一个互斥锁。结束。
  • 您是只读/写向量中的对象还是自己修改向量?如果是前者,则无需复制向量。顺便说一句:如果你在没有 std::reference_wrapper 的情况下使用 std::thread,无论如何你都会得到你的副本......

标签: c++ multithreading c++11 shared-ptr


【解决方案1】:

我需要使用某种锁定机制来规范对 SmallObj 的访问。我该怎么做?

为您的数据成员使用 getter 和 setter。使用std::mutex(或std::recursive_mutex,取决于是否需要递归锁定)数据成员来保护访问,然后始终使用锁定保护来锁定。

示例(另见代码中的 cmets):

class SmallObject{

    int getID() const{
        std::lock_guard<std::mutex> lck(m_mutex);
        return ....;
    }


    void setID(int id){
        std::lock_guard<std::mutex> lck(m_mutex);
        ....;
    }

    MyType calculate() const{
        std::lock_guard<std::mutex> lck(m_mutex);

        //HERE is a GOTCHA if `m_mutex` is a `std::mutex`
        int k = this->getID();               //Ooopsie... Deadlock

        //To do the above, change the decaration of `m_mutex` from
        //std::mutex, to std::recursive_mutex
}

private:
    ..some data
    mutable std::mutex m_mutex;
};

【讨论】:

  • 非常感谢您的回答!我曾考虑保护对SmallObj 的数据成员的访问,但我想知道在敏感范围内加锁的过程是否会增加调用的开销。这样做“贵”多少钱?
【解决方案2】:

最简单的解决方案是为整个向量保存一个std::mutex

#include <mutex>
#include <thread>
#include <vector>

void fun1(std::vector<std::shared_ptr<SmallObj>>& v,std::mutex &mtx) {
  for(int i=0; i<v.size(); i++)
    //Anything you can do before read/write of *v[i]...
   {
       std::lock_guard<std::mutex> guard(mtx);
        //read-write *v[i]
   }
   //Anything you can do after read/write of *v[i]...
}

void fun2(std::vector<std::shared_ptr<SmallObj>>& w,std::mutex &mtx) {
  for(int i=0; i<w.size(); i++) {

   //Anything that can happen before reading *w[i] 
   {
       std::lock_guard<std::mutex> guard(mtx);
        //read *w[i]
    }
    //Anything that can happen after reading *w[i]
}


int main() {

 std::mutex mtx;
 std::vector<std::shared_ptr<SmallObj>> tot;

 for(int iter=0; iter<iterMax; iter++) {
   for(int nObj=0; nObj<nObjMax; nObj++)
      .. create a SmallObj in the heap and store a shared_ptr in tot ..

   std::vector<std::shared_ptr<SmallObj>> v, w;
   .. copy elements of "tot" in v and w ..

   std::thread t1([&v,&mtx] { fun1(v,mtx); });
   std::thread t2([&w,&mtx] { fun2(w,mtx); });

   t1.join();
   t2.join();
 }

 return 0;
}

但是,您实际上只会在fun1()fun2() 循环中的前/后块中完成的位上获得任何并行性。

您可以通过引入更多锁来进一步提高并行度。 例如,您可以只使用 2 个控制奇偶元素的互斥锁:

void fun1(std::vector<int>&v,std::mutex& mtx0,std::mutex& mtx1 ){
    for(size_t i{0};i<v.size();++i){
        {
        std::lock_guard<std::mutex> guard(i%2==0?mtx0:mtx1);
            //read-write *v[i]
        }
    } 
}

fun2() 的格式类似。

您可以通过从向量的两端工作或使用try_lock 并移动到后续元素并在可用时“返回”到锁定元素来减少争用。

如果一个函数的迭代执行比另一个函数大得多,并且在另一个函数完成之前从“更快”的一个函数中获取结果有一些优势,那么这可能是最重要的。

替代方案:

显然可以为每个对象添加std::mutex。 这是否有效/是否有必要将取决于函数 fun1fun2 中实际执行的操作以及这些互斥锁的管理方式。

如果需要在任一循环开始之前锁定,实际上并行性可能没有任何好处,因为fun1()fun2() 中的一个将基本上等待另一个完成,而这两个实际上将串联运行.

【讨论】:

  • 非常感谢!我想我将只锁定部分功能,因为可能发生数据竞争的部分非常有限。在使用互斥锁锁定的过程中仍然有一些问题困扰着我:如果我锁定了一个指针向量,是内存 pointed 还是存储向量的内存或两者都被锁定?跨度>
  • @RiccardoGiubilato 太好了。这就是我试图克服的 - 你想将锁定限制在需要的地方。锁定的内容由您决定!您的设计决定了锁控制的内容,然后您必须遵守该原则来实施。因此,如果您说对象被锁定,那么您需要确保在没有持有锁的情况下不会更改对象。您可能(或没有)有另一个锁来更改指针,另一个锁来更改向量的结构,或者一个锁在所有这些上......
  • @RiccardoGiubilato 请查看文档,但请记住std::shared_ptr&lt;&gt; 上有一些并发保证。此外,只要您不更改std::vector&lt;&gt; 的大小,两个线程就可以同时读取它并且(在适当的锁定位置)同时更改不同的成员。目前尚不清楚您是否真的需要wv,或者是否需要一个std::vector&lt;&gt;
  • @RiccardoGiubilato 这里关于“线程安全”的部分应该会有所帮助。 en.cppreference.com/w/cpp/container
猜你喜欢
  • 2011-06-06
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2014-06-24
  • 2013-09-07
  • 1970-01-01
  • 1970-01-01
  • 2014-09-08
相关资源
最近更新 更多