这种无锁设计线程安全吗?

时间:2015-02-25 20:57:44

标签: c++ multithreading shared-ptr atomic

在不同的主题中,我会执行以下操作:

共享变量:

std::shared_ptr<Data> dataPtr;
std::atomic<int> version_number;

Thread1,生产者接收新数据并执行

dataPtr.reset(newdata);
version_number++;

消费者正在做的其他线程:

int local_version=0;
std::shared_ptr<Data> localPtr;
while(local_version!=version_number)
 localPtr=dataPtr;
 ...operation on my_ptr...
 localPtr.reset();
 local_version=version_number.load();

在这里我知道消费者可能会跳过某个版本,如果他们正在处理数据并且新的更新继续进行,那对我来说很好,我不需要它们来处理所有版本,只是最后一个版本。 我的问题是,这条线是原子的吗?

localPtr=dataPtr;

我是否总是获取dataPtr中的最新版本,或者是否会缓存或者可能导致我的设计出错?

THKS。

4 个答案:

答案 0 :(得分:3)

正如haavee指出的那样,多个线程可以安全地同时进行

localPtr = dataPtr;

因为只读取共享变量,并且在该过程中更新的共享元数据块具有特殊的线程安全保证。

但是,

之间存在竞争
dataPtr.reset(newdata); // in producer, a WRITE to the shared_ptr
localPtr = dataPtr;     // in consumer, an access to the same shared_ptr

所以这个设计不是线程安全的。

答案 1 :(得分:1)

根据http://en.cppreference.com/w/cpp/memory/shared_ptr:是的。 my_ptr = dataPtr是线程安全的。

  

所有成员函数(包括复制构造函数和复制赋值)   可以由shared_ptr的不同实例上的多个线程调用   即使这些实例是副本,也没有其他同步   并分享同一对象的所有权。

我们无法保证您认为自己正在加载的版本将是您要加载的版本;生产者设置指针和版本号的提升不是atomic操作,也不是消费者读取指针和消费者更新版本号。

答案 2 :(得分:0)

这段代码看起来很有意义。如果随机数量的消费者查看相同数据会有什么好处? (这是您的代码中会发生的事情,尽管是以技术上线程安全的方式。)

如果你想让第一个消费者获取其他人没有计划的数据,你可能想要从每个消费者原子地交换dataPtr for empty()shared_ptr。然后在交换之后,消费者检查他必须是非空的并进行计算。执行相同操作的所有其他消费者将在各自的交换后获得一个空的()共享ptr。

从您的代码中删除版本号后,您获得了一个免费使用锁定生产者消费者计划。

std::shared_ptr<Data> dataPtr;

void Producer()
{
    std::shared_ptr<Data> newOne = std::shared_ptr<Data>::make_shared();
    std::atomic_exchange(dataPtr, newOne);
}

// called from multiple threads
void Consumer()
{
    std::shared_ptr<Data> mine;
    std::atomic_exchange(mine,dataPtr);
    if( !mine.empty() )
    {  // compute, using mine. Only one thread is lucky for any given Data instance stored by producer.
    }
}

编辑: 从文档here看来,shared_ptr :: swap()不是原子的。相应地调整了代码。 EDIT2:制片人更正了。首先不要使用那些东西的另一个原因。

对于您描述的用例,当您真的不在乎某些消费者偶尔会错过某些内容时,这里是一个完整的实现,它将版本号与数据一起打包。该模板允许将其用于其他类型。也许还有一些构造函数,删除等等......

#include "stdafx.h"
#include <cstdint>
#include <string>
#include <memory>
#include <atomic>
#include <thread>
#include <vector>
#include <iostream>
#include <chrono>


template <class _T>
class Versioned
{
    _T m_data;
    uint32_t m_version;
    static std::atomic<uint32_t> s_next;
public: 
    Versioned(_T & data)
        : m_data(data)
        , m_version(s_next.fetch_add(1UL))
    {}
    ~Versioned()
    {

    }
    const _T & Data() const
    {
        return m_data;
    }
    uint32_t Version() const
    {
        return m_version;
    }
};

template <class _T>
std::atomic<uint32_t> Versioned<_T>::s_next;

typedef Versioned<std::string> VersionedString;

static volatile bool s_running = true;
static std::shared_ptr<VersionedString> s_dataPtr;

int _tmain(int argc, _TCHAR* argv[])
{
    std::vector<std::thread> consumers;
    for (size_t i = 0; i < 3; ++i)
    {
        consumers.push_back(std::thread([]()
        { 
            uint32_t oldVersion = ~0UL;
            std::shared_ptr<VersionedString> mine; 
            while (s_running)
            {
                mine = std::atomic_load(&s_dataPtr);
                if (mine)
                {
                    if (mine->Version() != oldVersion)
                    {
                        oldVersion = mine->Version();

                        // No lock taken for cout -> chaotic output possible.
                        std::cout << mine->Data().c_str();
                    }
                }
            }
        }));
    }

    for (size_t i = 0; i < 100; ++i)
    {
        std::shared_ptr<VersionedString> next = std::make_shared<VersionedString>(std::string("Hello World!"));
        std::atomic_store(&s_dataPtr, next);
        std::this_thread::sleep_for(std::chrono::milliseconds(100));
    }

    s_running = false;
    for (auto& t : consumers)
    {
        t.join();
    }

    return 0;
}

答案 3 :(得分:0)

从概念上讲,您的“无锁”方案只是浪费时间和CPU。

如果您不关心丢失中间版本,只需让您的生产者将其输出限制为消费者可以应对的频率,并使用共享队列或任何经过尝试和验证的任务间通信机制来传递数据包裹。

实时系统都是为了保证响应能力,而优秀的设计试图在其上设置一个合理的上限,而不是为了冷静而烧掉CPU。

C ++ 11和时尚的新“非阻塞”突发奇想通过引诱每个人和他的狗相信一些原子变量将解决每个同步问题而造成如此大的伤害。事实上,他们不会。