Home > Back-end >  Is there a C smart pointer that could wrap up an object to make it thread safe?
Is there a C smart pointer that could wrap up an object to make it thread safe?

Time:03-29

I wanted to ask if there is a smart pointer that could take in any class in its template and then any operations done with such pointer would result in a thread-safe operation.

Basically an idea would be that such pointer would automatically hold an internal lock during a scope and release it when the pointer goes out of scope.

Use case would be for example to pull such pointer from a static, pre-allocated array into some scope and perform thread-safe operations inside that scope on the object itself.

I tried to find a C library/feature that could perhaps allow for some thread-safe mutation on objects by wrapping it into a single smart pointer object.

CodePudding user response:

if there is a smart pointer that could take in any class in its template and then any operations done with such pointer would result in a thread-safe operation.

No, there is no such smart pointer in the C standard.

CodePudding user response:

I don't think that's possible in the "usual" smart pointer sense, because when doing ptr->something() or (*ptr).something(), the operator-> and operator* methods are called, they return the pointer/reference and then something is invoked, so you don't have any way to know when to unlock the mutex after the operation has been done. This can be worked around through proxy objects, but that's another can of worms, especially when mixed with usage of auto.

Moreover, on a higher level this is rarely a kind of thread-safety guarantee one actually needs. In a codebase of ours someone once wrote a wrapper for std::map with a mutex protecting some common mutation operations; this was eminently useless for several reasons. The most obvious was that operator[] returns a reference anyway (so, you get a reference that may be instantly invalidated by someone else calling e.g. erase()); but most importantly, people did stuff like if (!map.count(key)) { map[key].do_something(); }, ignoring the fact that the result of count became stale immediately.

The takeaway here is that generally mutex-wrapping single operations on an objects doesn't gain you much: to actually work safely in a sane manner usually you need to take a mutex for a longer period, to ensure your code has a consistent snapshot of the protected object state.


A possibility to attack both these problems is to turn the whole thing to a different angle: you may wrap your object in an "escrow" object that forces you to take the mutex to access the data, but also think in terms of "doing all the operations where you need it" in a single "mutex-take". A sketch may be something like:

template<typename T>
class MutexedPtr {
    std::mutex mtx;
    std::unique_ptr<T> ptr;
public:
    MutexedPtr(std::unique_ptr<T> ptr) : ptr(std::move(ptr)) {}
    

    template<typename FnT>
    void access(FnT fn) {
        std::lock_guard<std::mutex> lk(mtx);
        fn(*ptr);
    }
};

The usage should be something like:

MutexedPtr<Something> ptr = ...;
...
ptr.access([&](Something &obj) {
    // do your stuff with obj while the mutex is taken
});

whether this is something that could be useful to your use case is up to you.

CodePudding user response:

I wanted to ask if there is a smart pointer that could take in any class in its template and then any operations done with such pointer would result in a thread-safe operation.

Yes, that's possible. Here's a simple implementation:

#include <thread>
#include <mutex>
#include <cstdio>

template <class T>
struct SyncronizedPtrImpl {
private:
    std::scoped_lock<std::mutex> lock;
    T* t;

public:
    SyncronizedPtrImpl(std::mutex& mutex, T* t) : lock(mutex), t(t) {}

    T* operator->() const { return t; }
};


template <class T>
struct SyncronizedPtr {
private:
    std::mutex mutex;
    T* p;
public:

    SyncronizedPtrImpl<T> operator->() {
        return SyncronizedPtrImpl<T>{mutex, p};
    }

    SyncronizedPtr(T* p) : p(p) {}
    ~SyncronizedPtr() { delete p; }
};

int main() {
    struct Foo {
        int val = 0;
    };

    SyncronizedPtr ptr(new Foo);

    std::thread t1([&]{
        for (int i = 0; i != 10;   i)   ptr->val;
    });

    std::thread t2([&]{
        for (int i = 0; i != 10;   i) --ptr->val;
    });

    t1.join();
    t2.join();

    return ptr->val == 0;
}
  •  Tags:  
  • c
  • Related