Re: Why are Boost thread mutexes so slow compared to Pthreads?



On Dec 30, 6:46 pm, David Schwartz <dav...@xxxxxxxxxxxxx> wrote:
On Dec 30, 12:43 am, David Barrett-Lennard <davi...@xxxxxxxxxxxx>
wrote:

That is true if and only if the scope of the lexical lock is 100%
identical to the block. But as I explained, that's a hideously
deficient design pattern because it forces you into a procrustean
choice -- if you need to drop the lock inside the lexical scope, do
you re-architect the entire block and risk missing points that require
an unlock or do you use a sub-optimal solution that doesn't involve
dropping the lock?
It seems an unlikely scenario.

On the contrary, it's quite common. It occurs any time you need to do
something that is too lengthy to do while holding the lock, or even
any time the lock simply doesn't need to be held.

I don't believe it occurs when your initial design only locks the
mutex for logically indivisible tasks.

I don't understand the problem. Can you provide a reference?
The problem is that if you force a lock to match a lexical scope, you
have to rearchitect if you need to do something inside that scope that
cannot be done while holding the lock.
Do you have a realistic example?

I posted one before. Suppose you have a method that triggers an
outbound connection to a configured server. Previously, servers were
configured by IP. Someone wants to add a configuration by host name.
So now you have to add this code to the middle of a complex block done
while the object is locked:

if(object->HasHostName())
{
object->AddRef();
object->Unlock();
object->Resolve(); // takes too long, can't hold lock
object->Lock();
object->DecRef();

}

How do you do that deep inside a lexically-scoped lock? The answer is
that you'll probably just do it while holding the lock and hope for
the best. The whole point of a scoped lock is to hide all the unlock
points, so it's not like you're going to go finding them all.

What is the function of this "complex block" in which the object needs
to be locked? It seems like a melting pot given that it can change
over time to need to resolve a hostname in the middle of it. This
indicates poor decomposition. A better design would have identified
the atomic operations on the object making it easier for higher level
"orchestration" code to only acquire/release locks where logically
required.


The other problem is that
because you don't see an 'unlock' at the '}', it's easy to miss the
fact that code before the '}' runs with different locks held than code
after the '}'.
That's obviously subjective and I actually think it's quite the
opposite - I can more easily understand the scope of locks when
unlocking is automatic.

Really?

{
ScopedLock foo;
Bar();
// here
Baz();
Qux();

}

Is it safe for the invariants to be broken at the point I marked with
'here'? Assume 'Qux' restores them.

That question has nothing to do with the scope of the lock (which is
simply bounded by the braces).


And, again, the major problem is simply that an "can't forget to
unlock" abstraction is useless, because you always must restore
invariants precisely before you unlock. A helpfully-added forgotten
unlock will be disastrous if invariants are not restored. It's hard to
audit all unlock points to make sure that invariants are restored on
all of them because the abstraction deliberately hides unlock points.
You want to manually unlock so you can audit all unlock points!

Exactly. Because you have to do that anyway. Those are precisely the
points where the invariants must be restored.

Irrespective of threading, a programmer worth his salt should be able
to write a block of code that performs a well defined function, and
deals correctly with all errors and exceptional circumstances, and
leaves objects in an appropriate state no matter how a thread exits
from the block of code. This is bread and butter stuff for
programmers.

Sure, and this means that at every point where the lock is or may be
unlocked he makes sure the invariants are restored. And he audits all
of those points to make sure they're all correct.

When I said "Irrespective of threading" I meant there is no mutex to
lock. A programmer should be able to write a block or code correctly
that deals properly with all the possible exit points.

When a block of code declares a lock I don't permit any manual unlock
statements and I immediately know two things:
1) the scope of the lock encompasses all the code in the block
2) whenever a thread exits the block it must restore the invariant.

How can you find all the points that exit the block? Or is every
function a possible exit point? Or do you assume functions that don't
throw exceptions now never will?

I follow the ideology of Design by Contract.

The design must be provably correct. Often the design will include a
no-throw contractual specification on a given function. It is not
usually possible to weaken such an assumption without upsetting the
correctness of the design. That's why it was there in the first
place.

It would be silly to make assumptions about how the design will change
in the future. A good design doesn't tend to radically change basic
assumptions over time. Sometimes strong assumptions like a no-throw
specification on a function make a lot of sense and won't need to
change in the future.

Manual unlocking changes none of this.

For example, before calling any function that might throw an
exception, you have to restore invariants or catch the exception.
That's a bitch to debug/validate.
I cannot understand why you think manual unlocking makes this easier.
Can you post an example?

Sure. Your code:

{
ScopedLock foo;
Bar();
// here
Baz();
Qux();

}

You have no way to know whether or not invariants must be restored at
the marked point.

My code:

{
ScopedLock foo;
Bar();
// here
Baz();
Qux();
foo.Unlock();

}

Now, you know that invariants can be violated at the 'here' point.
Because there is no way to legally exit the scope (exiting with a lock
held is illegal).

That implication is false. Instead you know that you don't know that
the invariants are necessarily met at the 'here' point. i.e. you
basically can't make any assumptions at all.

If someone screwed up and 'Baz' throws with
invariants broken, my code will assert in a debug build. Yours will
crash horribly or behave unpredictably. Or:

{
NonScopedLock foo;
Bar();
if(x)
{
foo.Unlock();
Baz();
foo.Lock();
}
Qux();
foo.Unlock();

}

Here you know that invariants must be restored when calling 'Baz'.
And, as a bonus, 'Baz' is called without the lock held.

Agreed. The idea that your code encodes incontrovertible declarative
truths is very useful.

However I can achieve all the same benefits and more using better
declarative building blocks.

For example:

1) If I want to declare the fact that I know that I don't know that
the invariants are met at a given position I do nothing at all because
that is always the appropriate default assumption to make.

2) If I want to make it clear there are intermediate points where the
invariant is met then I simply open a new ScopedLock in a separate
block or function. Normally this happens in the original design
because there is an emphasis on finding the logically indivisible
functions on the protected state.

3) If I want to document the fact that there is a no-throw
specification on one or more statements then I use some facility to
declare that fact *directly*, and have it validated in the debug
build.

4) If I want to ensure that a checkable invariant is met on exit from
a lock then I declare an object on the frame in that same scope whose
destructor performs the check.

5) Since the work done during a lock can be identified with a simple
logically indivisible function it is highly unlikely that I would
unnecessarily call a function like Baz whilst holding a lock.

6) In the unlikely case that I felt uncomfortable with ensuring I had
considered all possible exit points, I could audit them in debug by
declaring an object on the frame for a class that forces me to call
some method like end() before the destructor.


The fact that you are prepared to drop the lock in order to call Baz
shows that you haven't clearly delineated a logically indivisible
function on the protected state. In my experience failing to do that
is often a symptom of poor design.


These are not good example becauses there shouldn't be a one-to-one
correspondence between scoped locks and proper locks. This is because
scoped locks are fundamentally conceptually broken.

You haven't shown that.

The concept is --
you hold a lock while the invariants are broken, you restore
invariants before you release a lock.

Yes. That's what I understand a ScopedLock to imply. So?

Scoped locks suggest the completely incorrect notion that you hold a
mutex while you perform a series of operations that do not break
invariants.

Why do you think that? I don't.

In general, there's rarely a need to hold the lock across
operations that do not break invariants and this should be a carefully
considered design choice, not a default.

Agreed. ScopedLocks don't influence that choice. i.e. you first
choose where locks are needed and then you simply use ScopedLocks
where required.


.



Relevant Pages

  • Re: Why are Boost thread mutexes so slow compared to Pthreads?
    ... choice -- if you need to drop the lock inside the lexical scope, ... The whole point of a scoped lock is to hide all the unlock ... invariants precisely before you unlock. ...  whenever a thread exits the block it must restore the invariant. ...
    (comp.programming.threads)
  • Re: Why are Boost thread mutexes so slow compared to Pthreads?
    ... let's say that there is no possibility of the lock not being released on ... lock will be released without the invariants being restored, ... They must make a conscious effort at every possible mutex unlock point ... restore the invariants. ...
    (comp.programming.threads)
  • Re: Why are Boost thread mutexes so slow compared to Pthreads?
    ... lock, how do you reliably figure out whether or not you held the lock ... That is true if and only if the scope of the lexical lock is 100% ... an unlock or do you use a sub-optimal solution that doesn't involve ... unlock will be disastrous if invariants are not restored. ...
    (comp.programming.threads)
  • Re: CSingleLock - known behaviour?
    ... second Lock() call to do nothing if the object is already locked. ... Is this known behaviour of CSingleLock? ... locking indicates less than optimal design. ... then you have called UnLock too many times. ...
    (microsoft.public.vc.mfc)
  • Re: Recursive mutex that can be waited upon (pthread)
    ... on an object, you are releasing the lock to the object, hence of course the object is free for being changed. ... Java's mistake, to be clear, is not necessarily the "full release" wait, but the fact that the language has no support at all for invariants and cannot possibly detect any problems. ... And on top of that the problems are difficult for programmers to avoid because they usually stem from using Java as it should be used -- which is to say that the methods you call are opaque and decoupled. ... However, most functions expect that their invariants are "clean" on entry and exit, and "static" while they hold the mutex. ...
    (comp.programming.threads)