简体   繁体   中英

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

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 ?

Thks.

As haavee points out, multiple threads can safely simultaneously do

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. my_ptr = dataPtr is thread-safe.

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.

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.

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

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. Adjusted the code accordingly. EDIT2: Producer corrected. 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.

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.

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. As a matter of fact, they won't.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

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