Date: Wed, 31 May 2023 22:34:21 -0700
On Wednesday, 31 May 2023 21:42:45 PDT Phil Bouchard wrote:
> > if (!container.empty())
> > container.push)back(1);
> >
> > is not safe.
>
> Why? The mutex is recursive so it just gives priority to the current
> thread for the scope of the condition.
Because the container may have become non-empty between the check and the
push_back(1), in which case the code violated the requirement to append 1 only
if it was empty.
> > And this of course becomes more complex when you have two or more
> > containers. Here's the first part of an algorithm: given containers C1
> > and C2, if the first element in C1 isn't in C2, then remove it from there
> > (pop_front) and append (push_bacl) to C2.
> >
> > If you realise what the second part will be, feel free to comment on it
> > and
> > explain how your API will ensure the user writes the proper code for that.
>
> // imagine both containers are locked here for the scope of the condition:
> if (find(C2.begin(), C2.end(), * C1.begin()) != C2.end())
> {
> T value = C1.front();
> C1.pop_front();
> C2.push_back(value);
> }
Locked how? That's the interesting part.
Are you suggesting that the compiler analyse the if-find line, find all lockable
elements in use there, then lock them behind the scenes without user action
and keep them locked until the end of the scope?
> Again, where's the problem if both containers feature a recursive mutex?
Recursion is not the problem. Atomicity of the action is. The point I tried to
make with the previous example is that one must reason about the duration of
the lock to decide when it must start and when it must end, so the conditions
don't change between statements. This is also the analogy of the transaction
that someone else posted in this thread.
With the simple examples we're discussing here, it might be obvious what to do
and thus make solutions obvious. What others and I are telling you is that
when it gets to really complex thread-safe code, you *have* to reason about
when locks must start and when they must end, and what other locks you have.
Plus, reason about the order of locks, to avoid deadlocks.
Thread-safety requires having the smallest possible critical sections, but no
smaller. If you pulverise your locks, you add overhead and actually lose
safety.
> Regarding teaching, this is higher-level programming so a new namespace
> should encompass these new classes.
That's not what I meant. You're oversimplifying the answer to a complex
question. Refer back to the top of this email:
if (!container.empty())
container.push)back(1);
This may have no data race and thus cause no data corruption, but it's not
what was required because the states may have changed. If this is still
allowed, then just using the container in question does *not* confer thread-
safety. And therefore, if it is allowed to compile, how do you propose we
teach everyone *how* to write code to actually make it thread-safe?
And how is that different from what we're already doing now?
> > if (!container.empty())
> > container.push)back(1);
> >
> > is not safe.
>
> Why? The mutex is recursive so it just gives priority to the current
> thread for the scope of the condition.
Because the container may have become non-empty between the check and the
push_back(1), in which case the code violated the requirement to append 1 only
if it was empty.
> > And this of course becomes more complex when you have two or more
> > containers. Here's the first part of an algorithm: given containers C1
> > and C2, if the first element in C1 isn't in C2, then remove it from there
> > (pop_front) and append (push_bacl) to C2.
> >
> > If you realise what the second part will be, feel free to comment on it
> > and
> > explain how your API will ensure the user writes the proper code for that.
>
> // imagine both containers are locked here for the scope of the condition:
> if (find(C2.begin(), C2.end(), * C1.begin()) != C2.end())
> {
> T value = C1.front();
> C1.pop_front();
> C2.push_back(value);
> }
Locked how? That's the interesting part.
Are you suggesting that the compiler analyse the if-find line, find all lockable
elements in use there, then lock them behind the scenes without user action
and keep them locked until the end of the scope?
> Again, where's the problem if both containers feature a recursive mutex?
Recursion is not the problem. Atomicity of the action is. The point I tried to
make with the previous example is that one must reason about the duration of
the lock to decide when it must start and when it must end, so the conditions
don't change between statements. This is also the analogy of the transaction
that someone else posted in this thread.
With the simple examples we're discussing here, it might be obvious what to do
and thus make solutions obvious. What others and I are telling you is that
when it gets to really complex thread-safe code, you *have* to reason about
when locks must start and when they must end, and what other locks you have.
Plus, reason about the order of locks, to avoid deadlocks.
Thread-safety requires having the smallest possible critical sections, but no
smaller. If you pulverise your locks, you add overhead and actually lose
safety.
> Regarding teaching, this is higher-level programming so a new namespace
> should encompass these new classes.
That's not what I meant. You're oversimplifying the answer to a complex
question. Refer back to the top of this email:
if (!container.empty())
container.push)back(1);
This may have no data race and thus cause no data corruption, but it's not
what was required because the states may have changed. If this is still
allowed, then just using the container in question does *not* confer thread-
safety. And therefore, if it is allowed to compile, how do you propose we
teach everyone *how* to write code to actually make it thread-safe?
And how is that different from what we're already doing now?
-- Thiago Macieira - thiago (AT) macieira.info - thiago (AT) kde.org Software Architect - Intel DCAI Cloud Engineering
Received on 2023-06-01 05:34:23