Date: Thu, 29 Jun 2023 07:51:18 +0200
On 29/06/2023 00.37, Frederick Virchanza Gotham via Std-Proposals wrote:
> A few months ago I was thinking of submitting this to Boost, but now
> I'm actually thinking maybe it should be in the Standard Library.
>
> I'm not the first person to come up with a class like this, but my own
> implementation is one of very few full implementations I've been able
> to find online.
>
> You define a global object as follows:
>
> Reservable< std::vector<int> > g_vec;
>
> From this point forward, multiple threads can access the object safely
> as follows:
>
> g_vec->push_back(42); // This is thread-safe
>
> Or you can do:
>
> auto mylock = g->vec.Reserve();
> mylock->push_back(44);
> mylock->push_back(47);
>
> Or if you want an L-value reference:
>
> auto mylock = g->vec.Reserve();
> auto &vec = *mylock;
> vec.push_back(44);
> vec.push_back(47);
>
I see two issues with this interface
In
> A few months ago I was thinking of submitting this to Boost, but now
> I'm actually thinking maybe it should be in the Standard Library.
>
> I'm not the first person to come up with a class like this, but my own
> implementation is one of very few full implementations I've been able
> to find online.
>
> You define a global object as follows:
>
> Reservable< std::vector<int> > g_vec;
>
> From this point forward, multiple threads can access the object safely
> as follows:
>
> g_vec->push_back(42); // This is thread-safe
>
> Or you can do:
>
> auto mylock = g->vec.Reserve();
> mylock->push_back(44);
> mylock->push_back(47);
>
> Or if you want an L-value reference:
>
> auto mylock = g->vec.Reserve();
> auto &vec = *mylock;
> vec.push_back(44);
> vec.push_back(47);
>
I see two issues with this interface
In
----
push_back(44);
push_back(47);
----
you are loking at every push_back, not ideal, and while looking at the
code it is not obvious.
Also you might have wanted 47 appearing in your vector after 44, since
vector is "thread-safe", it is not an unfair assumption, but it is not
necessarily true, as someone else might be pushing something in the
meantime, and thus introduce a value between 44 and 47.
With
----
> auto mylock = g->vec.Reserve();
> auto &vec = *mylock;
----
you avoid the issues mentioned earlier, but the scope of the lock is
generally too big, and it is easy to oversee it, the code should look
more like
----
// ...
{
auto mylock = g->vec.Reserve();
auto &vec = *mylock;
vec.push_back(44);
vec.push_back(47);
}
// ...
----
and I know most people will forget it.
Also it "leaks" the internal object, for example
----
auto vec& = *(g->vec.Reserve());
// bypassing mutex and still access to internal data
----
and also with
----
int& j = vec->at(0);
----
I think such classes are doomed, because hiding the fact there is a
synchronisation mechanism is rarely a good idea for low-level
operations/generic classes.
Also operator-> generally still leads to race conditions, it only helps
to avoid (some) data races
Which means that useful tools like sanitizers and valgrind cannot be
used anymore.
I believe a better approach is to provide a callback mechanism, with an
explicit lock function, for example
(https://fekir.info/post/sharing-data-between-threads/#_bind-the-data-and-mutex-together)
----
#include <mutex>
#include <type_traits>
template<class T, class mutex = std::mutex>
class mutexed_obj {
T obj;
mutable mutex m;
public:
template<typename... Args>
explicit mutexed_obj(Args&&... args) : obj(std::forward<Args>(args)...) {}
template<class F>
friend decltype(auto) lock(const mutexed_obj<T, mutex>& mo, F f){
std::scoped_lock l(mo.m);
return f(mo.obj);
}
template<class F>
friend decltype(auto) lock(mutexed_obj<T, mutex>& mo, F f){
std::scoped_lock l(mo.m);
return f(mo.obj);
}
};
----
usage:
mutexed_obj<int> data(42);
lock(data, [](int i){ std::cout << i;});
lock(data, [](int& i){ ++i;});
compared to your proposal:
1) you have always a given scope for a lock, making it easy to make it
minimal at every invocation
2) you can still leak an object, for example with
int& j = lock(data, [](int& i){ return i;});
int& j = 0;
lock(data, [&](int& i){ j=i });
but it is hopefully easier to verify that there are no leaks.
First, because there is always a "lock" function, which should be a
warning signal for most developer.
Second, because the class does not try to look like a normal class and
overload "operator->" and give internal access to the data to the
caller, but only though a callback.
Third, because thanks to the callback, it is harder to forget to make
the scope of the locked mutex too broad.
Also you can choose the mutex type, I would prefer not to have
recursive_mutex by default.
PS: I think there is a similar classes in a paper, I'm not able to find
it right now.
Received on 2023-06-29 05:51:25
