简体   繁体   English

此无锁设计线程安全吗?

[英]Is this lock free design thread safe?

In different threads I do the following: 在不同的线程中,我执行以下操作:

shared variable: 共享变量:

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

Thread1, the producer receive new data and do 线程1,生产者接收新数据并执行

dataPtr.reset(newdata);
version_number++;

Other threads, consumers are doing: 消费者正在执行的其他线程:

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();

Here I know that the consumers might skip some version, if they are processing data and new updates keep going, thats fine by me, i dont need them to process all versions, just the last available to them. 在这里,我知道使用者可能会跳过某些版本,如果他们正在处理数据并且新的更新持续进行,那对我来说很好,我不需要他们来处理所有版本,仅是他们可以使用的最后一个版本。 My question is, is this line atomic : 我的问题是,这行是原子的:

localPtr=dataPtr;

Will I always obtain the last version of what is in dataPtr or will this be cached or might lead to anything wrong in my design ? 我将始终获取dataPtr中最新版本的内容还是将其缓存或可能导致设计错误?

Thks. ks

As haavee points out, multiple threads can safely simultaneously do 正如haavee指出的那样,多个线程可以安全地同时执行

localPtr = dataPtr;

because the shared variable is only read, and the shared metadata block which is updated in the process has a special thread safety guarantee. 因为仅读取共享变量,并且在过程中更新的共享元数据块具有特殊的线程安全保证。

However, there is a race between 但是,在

dataPtr.reset(newdata); // in producer, a WRITE to the shared_ptr
localPtr = dataPtr;     // in consumer, an access to the same shared_ptr

so this design is NOT thread-safe. 因此此设计不是线程安全的。

According to http://en.cppreference.com/w/cpp/memory/shared_ptr : yes. 根据http://en.cppreference.com/w/cpp/memory/shared_ptr :是。 my_ptr = dataPtr is thread-safe. my_ptr = dataPtr是线程安全的。

All member functions (including copy constructor and copy assignment) can be called by multiple threads on different instances of shared_ptr without additional synchronization even if these instances are copies and share ownership of the same object. 多个线程可以在shared_ptr的不同实例上调用所有成员函数(包括副本构造函数和副本分配),而无需额外同步,即使这些实例是副本并共享同一对象的所有权。

There is no guarantee though that the version you think you're loading will be the one you will be loading; 虽然不能保证您认为要加载的版本将是您要加载的版本; the setting of the pointer by the producer and the the upping of the version number are not an atomic operation and neither is the reading of the pointer by the consumer and the updating of the version number by the consumer. 生产者对指针的设置以及版本号的增加不是atomic操作,消费者也不是指针读取以及消费者都不能更新版本号。

This code appears rather constructed to me. 这段代码对我来说似乎很虚构。 What would be the benefit if a random number of consumers looked at the same data? 如果随机数的消费者查看相同的数据有什么好处? (This is what would happen in your code, though in a technically thread-safe manner.) (尽管这在技术上是线程安全的,但是这将在您的代码中发生。)

If you want to have a first consumer takes data others don't kind of scheme, you might want to atomically swap the dataPtr for a empty() shared_ptr from each consumer. 如果要让第一个使用者使用其他人没有的方案的数据,则可能需要原子地将dataPtr交换为每个使用者的empty()shared_ptr。 Then after the swap the consumer checks for what he got to be non-empty and does the computation. 然后,在交换之后,消费者检查他必须非空的内容并进行计算。 All other consumers who did the same, will get an empty() shared ptr after their respective swap. 所有其他执行相同操作的消费者在各自进行交换后将获得empty()共享的ptr。

After removing the the version number from your code, you got a lock free- use-once producer consumer scheme. 从代码中删除版本号后,您便获得了一次锁定的一次免费使用的生产者消费者方案。

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.
    }
}

EDIT: As it turns out from documentation found here , shared_ptr::swap() is not atomic. 编辑:从这里找到的文档中可以看出,shared_ptr :: swap()不是原子的。 Adjusted the code accordingly. 相应地调整了代码。 EDIT2: Producer corrected. 编辑2:生产者已更正。 One more reason not to use that stuff in the first place. 还有一个理由不要首先使用这些东西。

For the use case you describe, when you really don't care if some consumers miss something now and then, here a complete implementation, which packs the version number together with the data. 对于您描述的用例,当您真的不在乎某些消费者是否时不时地错过某些东西时,这里是一个完整的实现,它将版本号与数据打包在一起。 The template allows to use that for other types as well. 该模板还允许将其用于其他类型。 Maybe with a few more constructors, deletes etc. added... 也许有更多的构造函数,删除等添加...

#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;
}

Conceptually your "lock free" scheme is just a waste of time and CPU. 从概念上讲,您的“无锁”方案只是浪费时间和CPU。

If you don't care about losing intermediate versions, simply have your producer limit its output to a frequency the consumers can cope with, and use a shared queue or any of the tried and proved inter-task communication mechanisms to pass data packets around. 如果您不担心丢失中间版本,只需让您的生产者将其输出限制为消费者可以应付的频率,并使用共享队列或任何经过验证的任务间通信机制来传递数据包。

Real-time systems are all about guaranteeing responsiveness, and a good design tries to put a reasonable cap on that instead of burning CPU for the sake of coolness. 实时系统都是关于确保响应能力的,一个好的设计试图在此之上设置一个合理的上限,而不是为了保持冷静而烧掉CPU。

C++11 and the new "non blocking" whim of fashion are doing so much harm by luring every man and his dog into believing a couple of atomic variables will solve every synchronization problem. C ++ 11和新的“非阻塞”时尚狂潮通过诱使每个人和他的狗相信几个原子变量将解决每个同步问题而造成了极大的伤害。 As a matter of fact, they won't. 事实上,他们不会。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM