All Your Thread Are Belong to Us

  • Thread starter Thread starter C# Learner
  • Start date Start date
Jon Skeet said:
...
Having read your example to Bruno, I see what you mean. I guess that
means only Waiting on the "innermost" lock.

Really? Same example as above, but this time they wait/pulse the innermost
lock:

Thread A aquires L1, acquires L2, then waits for L2 (releasing L2, not L1)
Thread B tries to acquire L1 -> waiting for Thread A, can't pulse L2

You are right: Strictly speaking, this is no deadlock, as some other thread
could acquire and Pulse L2; In practice however, there might well be no
other piece of code that pulses that lock...

Niki

PS: I've tried that sample; The docs are correct about the behaviour of
Wait - it doesn't touch any of the other locks
 
Jon Skeet said:
...
Yup - I'm going to mention that in a section about Control.Invoke.
Another problem with Control.Invoke is doing something like:
...
Yup, that's coming later.
...

Seems like you got big plans for that article...
Glad it's of some use. I really *must* get it finished though.

If you're done, think about an "advanced topics" sequel: Things like
ThreadPool, asynchronous delegates and asynchronous IO callbacks really do
need some good introduction, too
;-)

Niki
 
Niki Estner said:
Where is that information from? The Docs clearly have a different position:
"Wait releases the lock for the specified object only; if the caller is the
owner of locks on other objects, these locks are not released" (copied from
MSDN Article - Monitor.Wait Method)
Let's assume we have two locks, L1 and L2:
Thread A aquires L1, acquires L2, then waits for L1 (accoring to the docs
releasing L1, not L2)
Thread B acquires L1, tries to acquire L2 -> deadlock (Thread B is waiting
for thread A, and noone else could Pulse L1, so they both wait forever)


Not at all: calling Wait while holding multiple locks isn't good style
anyway; Shared resources should be locked for as short as possible.

Ooops. I was not very awake when I wrote this. You are right.

I got confused by the fact that if the same lock has been acquired N times,
Wait releases it completely and then reaquires it N times, but what I said
about the L1 / L2 case is completely wrong, and your deadlock example is
right.
There's nothing wrong about callbacks - synchronized containers or older COM
objects will have the same problems in ordinary function calls.

I agree, but the problem is more about "who" writes the code. When you call
a private or non virtual method, you have complete control on the locks that
this method will acquire, but when you call a callback or a virtual method
that may be overriden in assemblies written by someone else, you call some
piece of code that may try to acquire other locks and that will cause
deadlocks if the person who implements this piece of code is not aware that
it is being called in the context of a lock. There are two ways around it:
a) documentation b) rewrite the code so that the callback is done outside
the lock context. Of course, this does not happen in every piece of code but
this is something that people who write toolkits and framework components
should be aware of.

Bruno.
 
Jon Skeet said:
Having read your example to Bruno, I see what you mean. I guess that
means only Waiting on the "innermost" lock.

Yes, my post was totally confusing.
 
Niki Estner said:
Really? Same example as above, but this time they wait/pulse the innermost
lock:

Thread A aquires L1, acquires L2, then waits for L2 (releasing L2, not L1)
Thread B tries to acquire L1 -> waiting for Thread A, can't pulse L2

You are right: Strictly speaking, this is no deadlock, as some other thread
could acquire and Pulse L2; In practice however, there might well be no
other piece of code that pulses that lock...

Yup. When Wait and Pulse are involved, I suspect the rules need to be
extended quite a lot - unless (as may be the case) it's worth
recommending that when using Wait and Pulse, *no* other locks should be
held.

The rule about acquiring locks only in a particular order can be
generalised to "don't acquire lock A within lock B; you can acquire
lock B without having acquired lock A, but that's all". That would get
round the above, but it's a bit harder to keep track of. Mind you,
no-one's trying to say that it's easy to get all this right :)
 
Bruno Jouhier said:
...
I agree, but the problem is more about "who" writes the code. When you call
a private or non virtual method, you have complete control on the locks that
this method will acquire, but when you call a callback or a virtual method
that may be overriden in assemblies written by someone else, you call some
piece of code that may try to acquire other locks and that will cause
deadlocks if the person who implements this piece of code is not aware that
it is being called in the context of a lock.

I'm not sure I understand: My locks are almost always around "someone
else's" code: if that code is not considered thread-safe. That's what locks
are for, aren't they? Locking file streams, sockets, container classes, etc.
There are two ways around it:
a) documentation b) rewrite the code so that the callback is done outside
the lock context. Of course, this does not happen in every piece of code but
this is something that people who write toolkits and framework components
should be aware of.

Of course: Everything that's inside a lock should have a damn good reason to
be there. If a callback (or any other piece of code that takes time) can
live happily outside the lock, then it shouldn't be inside - for safety and
efficiency reasons.

Niki
 
The rule about acquiring locks only in a particular order can be
generalised to "don't acquire lock A within lock B; you can acquire
lock B without having acquired lock A, but that's all". That would get
round the above, but it's a bit harder to keep track of. Mind you,
no-one's trying to say that it's easy to get all this right :)

That's the rule I am using. It is equivalent to saying that you always need
to acquire them in the same order, if you consider that "reacquiring" is
equivalent to "acquiring" (instead of considering that "reacquiring" is a
nop).

Unfortunately, there is no way to verify this at compile time. But you can
write assertions that check it at run-time. For example, you can encapsulate
your locks in properties and encode these rules in the accessors:

object LockB { get { return _lockB; } }
object LockA { get { Assert(!IsAcquired(_lockB)); return _lockA; } }

Unfortunately (again), there is no API to check if a lock has been acquired
or not (I submitted a request for this on the Whidbey NG), but you can get
around it with:

static bool IsAcquired(object l) { try { Monitor.Pulse(l); return
true; } catch (SynchronizationLockException) { return false; } }

You should only activate these checks in debug mode because they slow things
down a lot (you end up throwing and catching an exception every time you use
LockA).

I have used assertions like these in my multi-threaded server and I found
them very helpful, especially when I had to reorganize code. I did not have
to go through extensive stress testing to detect deadlock conditions, I
found them as soon as I tried to execute any code path that did not acquire
the locks in the right order.

Bruno.
 
Back
Top