Thread in C# - Part 8: Synchronization Contexts  
 

(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)


 
 

 
     
 
Công nghệ khác:


Thread in C# - Part 7: Wait HandlesThread in C# - Part 6: Thread State
Thread in C# - Part 5: Interrupt and AbortThread in C# - Part 4: Locking and Thread Safety
Thread in C# - Part 3: Synchronization EssentialsThread in C# - Part 2: Creating and Starting Threads
  Xem tiếp    
 
Lịch khai giảng của hệ thống
 
Ngày
Giờ
T.Tâm
TP Hồ Chí Minh
Hà Nội
 
   
New ADSE - Nhấn vào để xem chi tiết
Mừng Sinh Nhật Lần Thứ 20 FPT-APTECH
Nhấn vào để xem chi tiết
Bảng Vàng Thành Tích Sinh Viên FPT APTECH - Nhấn vào để xem chi tiết
Cập nhật công nghệ miễn phí cho tất cả cựu sinh viên APTECH toàn quốc
Tiết Thực Vì Cộng Đồng
Hội Thảo CNTT
Những khoảnh khắc không phai của Thầy Trò FPT-APTECH Ngày 20-11