(Post 26/10/2007) Rather than locking manually,
one can lock declaratively. By deriving from ContextBoundObject and applying
the Synchronization attribute, one instructs the CLR to apply locking
automatically.
Here's an example:
using System;
using System.Threading;
using System.Runtime.Remoting.Contexts;
[Synchronization]
public class AutoLock : ContextBoundObject {
public void Demo() {
Console.Write ("Start...");
Thread.Sleep (1000); // We can't be preempted here
Console.WriteLine ("end"); // thanks to automatic locking!
}
}
public class Test {
public static void Main() {
AutoLock safeInstance = new AutoLock();
new Thread (safeInstance.Demo).Start(); // Call the Demo
new Thread (safeInstance.Demo).Start(); // method 3 times
safeInstance.Demo(); // concurrently.
}
}
Start... end
Start... end
Start... end |
The CLR ensures that only one thread can execute code
in safeInstance at a time. It does this by creating a single synchronizing
object – and locking it around every call to each of safeInstance's methods
or properties. The scope of the lock – in this case – the safeInstance
object – is called a synchronization context.
So, how does this work? A clue is in the Synchronization
attribute's namespace: System.Runtime.Remoting.Contexts. A ContextBoundObject
can be thought of as a "remote" object – meaning all method
calls are intercepted. To make this interception possible, when we instantiate
AutoLock, the CLR actually returns a proxy – an object with the same methods
and properties of an AutoLock object, which acts as an intermediary. It's
via this intermediary that the automatic locking takes place. Overall,
the interception adds around a microsecond to each method call.
Automatic synchronization cannot be used to
protect static type members, nor classes not derived from ContextBoundObject
(for instance, a Windows Form). |
The locking is applied internally in the same way. You
might expect that the following example will yield the same result as
the last:
[Synchronization]
public class AutoLock : ContextBoundObject {
public void Demo() {
Console.Write ("Start...");
Thread.Sleep (1000);
Console.WriteLine ("end");
}
public void Test() {
new Thread (Demo).Start();
new Thread (Demo).Start();
new Thread (Demo).Start();
Console.ReadLine();
}
public static void Main() {
new AutoLock().Test();
}
}
(Notice that we've sneaked in a Console.ReadLine statement).
Because only one thread can execute code at a time in an object of this
class, the three new threads will remain blocked at the Demo method until
the Test method finishes – which requires the ReadLine to complete. Hence
we end up with the same result as before, but only after pressing the
Enter key. This is a thread-safety hammer almost big enough to preclude
any useful multithreading within a class!
Furthermore, we haven't solved a problem described earlier:
if AutoLock were a collection class, for instance, we'd still require
a lock around a statement such as the following, assuming it ran from
another class:
if (safeInstance.Count >
0) safeInstance.RemoveAt (0);
unless this code's class was itself a synchronized ContextBoundObject!
A synchronization context can extend beyond the scope
of a single object. By default, if a synchronized object is instantiated
from within the code of another, both share the same context (in other
words, one big lock!) This behavior can be changed by specifying an integer
flag in Synchronization attribute's constructor, using one of the constants
defined in the SynchronizationAttribute class:
Constant |
Meaning |
NOT_SUPPORTED |
Equivalent to not using the Synchronized attribute |
SUPPORTED |
Joins the existing synchronization context if instantiated from
another synchronized object, otherwise remains unsynchronized |
REQUIRED(default) |
Joins the existing synchronization context if instantiated from
another synchronized object, otherwise creates a new context |
REQUIRES_NEW |
Always creates a new synchronization context |
So if object of class SynchronizedA instantiates an object of class SynchronizedB,
they'll be given separate synchronization contexts if SynchronizedB is
declared as follows:
[Synchronization (SynchronizationAttribute.REQUIRES_NEW)]
public class SynchronizedB : ContextBoundObject { ...
The bigger the scope of a synchronization context, the
easier it is to manage, but the less the opportunity for useful concurrency.
At the other end of the scale, separate synchronization contexts invite
deadlocks. Here's an example:
[Synchronization]
public class Deadlock : ContextBoundObject {
public DeadLock Other;
public void Demo() { Thread.Sleep (1000); Other.Hello(); }
void Hello() { Console.WriteLine ("hello"); }
}
public class Test {
static void Main() {
Deadlock dead1 = new Deadlock();
Deadlock dead2 = new Deadlock();
dead1.Other = dead2;
dead2.Other = dead1;
new Thread (dead1.Demo).Start();
dead2.Demo();
}
}
Because each instance of Deadlock is created within Test
– an unsynchronized class – each instance will gets its own synchronization
context, and hence, its own lock. When the two objects call upon each
other, it doesn't take long for the deadlock to occur (one second, to
be precise!) The problem would be particularly insidious if the Deadlock
and Test classes were written by different programming teams. It may be
unreasonable to expect those responsible for the Test class to be even
aware of their transgression, let alone know how to go about resolving
it. This is in contrast to explicit locks, where deadlocks are usually
more obvious.
Reentrancy
A thread-safe method is sometimes called reentrant, because
it can be preempted part way through its execution, and then called again
on another thread without ill effect. In a general sense, the terms thread-safe
and reentrant are considered either synonymous or closely related.
Reentrancy, however, has another more sinister connotation
in automatic locking regimes. If the Synchronization attribute is applied
with the reentrant argument true:
[Synchronization(true)]
then the synchronization context's lock will be temporarily
released when execution leaves the context. In the previous example, this
would prevent the deadlock from occurring; obviously desirable. However,
a side effect is that during this interim, any thread is free to call
any method on the original object ("re-entering" the synchronization
context) and unleashing the very complications of multithreading one is
trying to avoid in the first place. This is the problem of reentrancy.
Because [Synchronization(true)] is applied
at a class-level, this attribute turns every out-of-context method
call made by the class into a Trojan for reentrancy. |
While reentrancy can be dangerous, there are sometimes
few other options. For instance, suppose one was to implement multithreading
internally within a synchronized class, by delegating the logic to workers
running objects in separate contexts. These workers may be unreasonably
hindered in communicating with each other or the original object without
reentrancy.
This highlights a fundamental weakness with automatic
synchronization: the extensive scope over which locking is applied can
actually manufacture difficulties that may never have otherwise arisen.
These difficulties – deadlocking, reentrancy, and emasculated concurrency
– can make manual locking more palatable in anything other than simple
scenarios.
(Sưu tầm) |