【问题标题】:Thread safety in std::map of std::shared_ptrstd::shared_ptr 的 std::map 中的线程安全
【发布时间】:2016-07-12 19:55:23
【问题描述】:

我知道周围有很多类似的问题有答案,但由于我仍然不了解这个特殊情况,所以我决定提出一个问题。

我拥有的是 shared_ptrs 到动态分配数组 (MyVector) 的映射。我想要的是无需锁定的有限并发访问。我知道地图本身不是线程安全的,但我一直认为我在这里所做的应该没问题,即:

我在这样的单线程环境中填充地图:

typedef shared_ptr<MyVector<float>> MyVectorPtr;

for (int i = 0; i < numElements; i++)
{
    content[i] = MyVectorPtr(new MyVector<float>(numRows));
}

初始化后,我有一个线程从元素中读取,一个线程替换 shared_ptrs 指向的内容。

线程 1:

for(auto i=content.begin();i!=content.end();i++)
{
    MyVectorPtr p(i->second);
    if (p)
    {
        memory_use+=sizeof(int) + sizeof(float) * p->number;
    }
}

线程 2:

    for (auto itr=content.begin();content.end()!=itr;++itr)
    {
        itr->second.reset(new MyVector<float>(numRows));
    }

一段时间后,我在两个线程之一中遇到了段错误或双重空闲。不知何故,这并不奇怪,但我还是不太明白。

我认为这可行的原因是:

  1. 我没有在多线程中添加或删除地图的任何项目 环境,所以迭代器应该总是指向有效的东西。
  2. 我认为只要操作是原子的,同时更改地图的单个元素就可以了。
  3. 我认为我在 shared_ptr 上执行的操作(增加线程 1 中的引用计数、减少线程 2 中的引用计数)是原子的。 SO Question

很明显,要么我的一个或多个假设是错误的,要么我没有做我认为的那样。我认为重置实际上不是线程安全的,std::atomic_exchange 有帮助吗?

有人可以释放我吗?非常感谢!

如果有人想尝试,这里是完整的代码示例:

#include <stdio.h>
#include <iostream>
#include <string>
#include <map>
#include <unistd.h>
#include <pthread.h>


using namespace std;

template<class T>
class MyVector
{
public:
    MyVector(int length)
    : number(length)
    , array(new T[length])
    {
    }

    ~MyVector()
    {
        if (array != NULL)
        {
            delete[] array;
        }
        array = NULL;
    }

    int number;

private:
    T* array;
};

typedef shared_ptr<MyVector<float>> MyVectorPtr;


static map<int,MyVectorPtr> content;
const int numRows = 1000;
const int numElements = 10;

//pthread_mutex_t write_lock;

double get_cache_size_in_megabyte()
{
    double memory_use=0;
    //BlockingLockGuard guard(write_lock);
    for(auto i=content.begin();i!=content.end();i++)
    {
        MyVectorPtr p(i->second);
        if (p)
        {
            memory_use+=sizeof(int) + sizeof(float) * p->number;
        }
    }

    return memory_use/(1024.0*1024.0);

}


void* write_content(void*)
{
    while(true)
    {
        //BlockingLockGuard guard(write_lock);
        for (auto itr=content.begin();content.end()!=itr;++itr)
        {
            itr->second.reset(new MyVector<float>(numRows));
            cout << "one new written" <<endl;
        }

    }
    return NULL;
}

void* loop_size_checker(void*)
{
    while (true)
    {
        cout << get_cache_size_in_megabyte() << endl;;
    }
    return NULL;
}

int main(int argc, const char* argv[])
{
    for (int i = 0; i < numElements; i++)
    {
        content[i] = MyVectorPtr(new MyVector<float>(numRows));
    }

    pthread_attr_t attr;
    pthread_attr_init(&attr) ;
    pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED);
    pthread_attr_setscope(&attr, PTHREAD_SCOPE_SYSTEM);

    pthread_t *grid_proc3 = new pthread_t;
    pthread_create(grid_proc3, &attr, &loop_size_checker,NULL);

    pthread_t *grid_proc = new pthread_t;
    pthread_create(grid_proc, &attr, &write_content,(void*)NULL);


    // to keep alive and avoid content being deleted
    sleep(10000);
}

【问题讨论】:

  • MyVector 的 3/5/0 规则...(为什么不使用 std::vector?)
  • if (array != NULL) -- 调用delete[]时不需要检查NULL。
  • shared_ptr::operator=(const shared_ptr &amp;other) 如果在另一个线程中分配other,则不是线程安全的。 std::atomic&lt;std::shared_ptr&lt;X&gt;&gt; 可能不会少锁,所以我想如果你想少锁,你需要自己管理内存。
  • MyVector 是对的,这是一个虚拟类,我用它来表示我在生产代码中遇到的问题。我将编辑该示例,但仍然添加一个复制约束。等对主要问题没有帮助。
  • 实际上,我认为编写原子shared_ptr::operator=(const shared_ptr &amp;other) 是不可能的,因为只有在知道对象还活着的情况下才能增加引用计数。阅读other 将毫无意义,因为在您实际使用它之前,它可能已被释放。如果没有跨国记忆,您就不能原子地读取一个内存位置并修改另一个。在 x86 上,只能使用 TSX。

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


【解决方案1】:

我认为只要操作是原子的,同时更改地图的单个元素就可以了。

除非你有像std::atomic 这样的原子类型,否则更改地图中的元素不是原子的。

我认为我对 shared_ptr 执行的操作(增加 ref 计数,减少线程 1 中的 ref 计数,在线程 2 中重置)是原子的。

没错。不幸的是,您也在更改基础指针。该指针不是原子的。由于它不是原子的,因此您需要同步。

您可以做的一件事是使用std::shared_ptr 引入的atomic free functions。这将使您避免使用mutex

【讨论】:

  • 如何在 shared_ptr 上调用 reset 而不更改底层指针?
  • 我研究过的原子函数,但根据@Dani 的说法,它们并没有真正解决我的问题。
【解决方案2】:

让我们扩展在线程 1 上运行的 MyVectorPtr p(i-&gt;second);

为此调用的构造函数是:

template< class Y > 
shared_ptr( const shared_ptr<Y>& r ) = default;

这可能归结为underlying shared pointerreference count 的两个分配。

线程 2 很可能会删除共享指针,而在线程 1 中,指针被分配给 p。存储在shared_ptr 中的底层指针不是原子的。

因此,std::shared_ptr 的使用不是线程安全的。只要您不更新或修改底层指针,它就是线程安全的。

【讨论】:

    【解决方案3】:

    TL;DR;

    更改std::map 不是线程安全的,而使用std::shared_ptr 来处理其他引用是。

    您应该使用适当的同步机制来保护访问您的地图的读/写操作,例如std::mutex

    此外,如果std::shared_ptr 引用的实例的状态发生变化,如果从并发线程访问它,则需要防止数据争用。


    顺便说一句,您展示的MyVector 是一种过于幼稚的实现方式。

    【讨论】:

    • 如果我是对的,我不改变地图,而只改变地图中的std::shared_ptr,那为什么不起作用呢?
    • 我知道加锁可以解决问题,但如果不是绝对必要的话,我想不加锁。
    猜你喜欢
    • 1970-01-01
    • 2019-06-07
    • 2013-01-07
    • 2013-02-10
    • 1970-01-01
    • 2013-05-05
    • 1970-01-01
    相关资源
    最近更新 更多