2

We offer a package which interfaces with a bunch of other packages who's APIs are not thread safe. Our package's API is entirely message based and therefore asynchronous to allow thread safety for users of our package. Our package therefore wraps a bunch of non-thread safe packages and offers a thread-safe API. That means that users of our package can interface with our package from any thread.

We would like to add synchronous APIs to our package while maintaining thread safety. I've done some research and have come up with two possible patterns to do this which I will share below. I'm not entirely happy with either approach so am wondering if the community may have more suggestions for patterns we can use. Note that the code below is for design and illustration purposes (c++ pseudocode) so is not meant to compile.

Approach 1 - Package users dependency inject data access classes to our package. We access these classes using run time type inference.

// Define an interface class for all data accessor classes
class DataAccessor
{

}

// Some random data
class FooData
{
    int foo;
    int bar;
}

// A concrete data accessor
class FooDataAccessor : public DataAccessor
{
public:
    FooData getFooData()
    {
        FooData fooDataCopy;
        {
            //Locks cachedFooData in this scope
            ScopedCriticalSection _(dataCriticalSection);
            fooDataCopy.foo = cachedFooData.foo;
            fooDataCopy.bar = cachedFooData.bar;
        }
        return fooDataCopy;
    }
    void setFooData(FooData& fooData)
    {
        //Locks cachedFooData in this scope
        ScopedCriticalSection _(dataCriticalSection);
        cachedFooData.foo = dooData.foo;
        cachedFooData.bar = dooData.bar;
    }

private:
    FooData cachedFooData;
    CriticalSection dataCriticalSection; //Use this for locking cachedFooData to set the data.
}

class OurPackage
{
    OurPackage(std::vector<DataAccessor*>); //constructor which is injected the data accessors so that our package customers can own their lifecycle.
}

// How package users would inject the data accessors into our package, then later access the data
int main()
{
    std::vector<DataAccessor*> dataAccessors;
    //The package customer now populates the data Accessors with the instances they need.
    dataAccessors.push_back(new FooDataAccessor());        

    auto package = new OurPackage(dataAccessors);

    // How package users access the data, assume FooDataAccessor was at the front
    auto fooAccessor = dataAccessors.front();
    if (fooAccessor)
    {
        FooData data = fooAccessor->getFooData();
    }
}

// In OurPackage, set the actual data in caches
for (DataAccessor* dataAccessor : dataAccessors)
{
    //Use RTTI to find the instance we want
    if (auto dataAccessorTypeWeWant = dynamic_cast<DataAccessorTypeWeWant*>(dataAccessor) != nullptr)
    {
        //Set the data on dataAccessorTypeWeWant 
        //For example, set FooData
        FooData fooData;
        fooData.foo = 1;
        fooData.bar = 2;
        dataAccessorTypeWeWant.setFooData(fooData);
        break;
    }
}

2 - Use a singleton pattern instead

If the data access caches are singletons instead, package users don't have to manage the lifecycle of these classes and don't need to worry about passing pointers to instances of the data access caches around their application. This has all the pitfalls of singletons though.

Justin
  • 447
  • 4
  • 10
  • 33
  • It is unclear to me what exactly you want to achieve. Which parts of that code you posted are meant to be written by the user and what are the unsafe operations that need to be guarded? – user1531083 May 09 '17 at 09:43
  • The code meant to be written by the user are in int main(). That is, injecting the data accessor instances into our package and then accessing the data from those data accessors. The unsafe operations that we need to guard from are setting/getting the data in the data accessors. The data in there needs to be locked appropriately since it can be accessed and set from different threads. Does that make sense @user1531083 ? – Justin May 09 '17 at 17:41
  • Why don't you know what data you need in your API? Why can't Dataaccessor have a getFooData method? It feels like there is some other concern that is not explained. Why can't OurPackage take a single reference to DataAccessor? Ie. explicit OurPackage(const DataAccessor&) ? – Andrew May 09 '17 at 20:59
  • DataAccessor is an interface for various data accessors. Then you might have a class FooDataAccessor : public DataAccessor which exposes all things Foo-related and a class BarDataAccessor : public DataAccessor which exposes all things Bar related. These two data accessors might belong to different modules. – Justin May 10 '17 at 17:41
  • So FooDataAccessor is written by the user too? – user1531083 May 10 '17 at 18:33
  • No FooDataAccessor and BarDataAccessor are written by our package so we can expose the Foo/Bar cached data from our package. – Justin May 10 '17 at 19:41
  • 1
    Please don't post pseudo code. Your words are not enough to explain what you want, and the pseudo code doesn't help because entire sections make no sense (as they are "not intended to compile", and hence nonsense that looks like C++). I cannot tell what arguments are in, what are out arguments (did you mean to pass that vector by value, or not? Who populates it? Etc etc etc.), what you expose in data accessors and what you don't, how injecting is suppossed to make asynchronous stuff synchronous, or pretty much any question of any value can be settled by that pseudo code. – Yakk - Adam Nevraumont May 11 '17 at 17:47
  • Heck you are using "thread safe" as if it explains things. Thread safety is a property of operations on libraries with respect to constraints, not of libraries. I guess you mean "wrapped in a mutex so only one thread can do anything at any time, for the lifetime of certain operations deemed atomic"? And that these operations are the "Get" operations? But that is me inventing details that are lacking. – Yakk - Adam Nevraumont May 11 '17 at 17:51
  • @Yakk as per the stackoverflow meta, posting pseudocode is perfectly reasonable for design questions. What I mean by thread safe is that the data in the data accessors can be safely set and retrieved from any thread, without the classes interfacing with the data accessors having to worry about it. So the thread safety is all implemented inside the data accessors. In any case, I'll edit to clear up some of your questions. – Justin May 11 '17 at 20:57

2 Answers2

3

Whatever patter that you chose you should use an Atomic Type that is founded into the library <atomic>, functionality available since C++11. Whit this type you can create threadsafe variables, for example:

// Some random data
class FooData
{
    std::atomic<int>  foo;
    std::atomic<int>  bar;
}

I share you a description of this library from CPlusPlus:

Atomic types are types that encapsulate a value whose access is guaranteed to not cause data races and can be used to synchronize memory accesses among different threads.

Jorge Omar Medra
  • 978
  • 1
  • 9
  • 19
0

This is a sample. If copy by value is what you need.

#include <vector>
#include <iostream>
#include <thread>
#include <atomic>
#include <mutex>

// Undefine to see data corruptions
#define USING_LOCK

std::atomic<int> atomic_i;

class FooData {
public:
    FooData() :foo(atomic_i.fetch_add(1, std::memory_order_relaxed)), bar(foo) {}
    ~FooData() { if (foo != bar) { std::cout << "Data corrupted!\n"; } }
private:
    int foo;
    int bar;
};

class FooDataAccessor {
public:
    FooData getFooData() {
#ifdef USING_LOCK
        std::lock_guard<std::mutex> l(_lock);
#endif // USING_LOCK
        return cachedFooData;
    }
    void setFooData(const FooData& fooData) {
#ifdef USING_LOCK
        std::lock_guard<std::mutex> l(_lock);
#endif // USING_LOCK
        cachedFooData = fooData;
    }

private:
    FooData cachedFooData;
#ifdef USING_LOCK
    std::mutex _lock;
#endif // USING_LOCK
};

void f(FooDataAccessor* accessor) {
    for (size_t i = 0; i < 1000; i++) {
        accessor->getFooData();
        accessor->setFooData(FooData());
    }
}

int main() {
    FooDataAccessor accessor;
    std::vector<std::thread> v;
    for (size_t i = 0; i < 5; i++) {
        v.emplace_back(f, &accessor);
    }
    for (auto& t : v) {
        t.join();
    }
}
bitlixi
  • 610
  • 7
  • 10