SyncBlocks and Critical Sections
This article is an extension to an earlier one I wrote on locks. A reader requested that I explain SyncBlocks too, so here it is.
Simply put, SyncBlocks are how mutual exclusion of critical sections are achieved in .net (using Monitors and locks). SyncBlocks are basically data structures that the runtime maintains to store information about objects and among other things it maintains the lock state of an object.
When an object is created on the managed heap, the runtime automatically adds couple of fields to an object. One of them is the SyncBlockIndex. (The other is the method table pointer, but we will leave that out of this discussion). When an object is initialized, it is assigned some negative value which denotes that the object has not been assigned a SyncBlock (and hence not locked yet). When a thread acquires a lock on an object (by calling Monitor.Enter), the CLR allocates a SyncBlock (from its cache of SyncBlocks) to the object. The index of that allocated block is assigned to the SyncBlockIndex. Once the thread releases the lock (by calling Monitor.Exit), the SyncBlock is freed and the index of the object is reset to a negative value. The released SyncBlock is returned to the pool of SyncBlocks and is free to be allocated to another object.
With this approach, there is very little overhead; a Syncblock is only allocated when a lock is acquired on an object and can be reused on release.
Remember that a thread that has acquired the lock on object can make additional calls to Monitor.Enter without blocking itself. But it has to call Monitor.Exit the same number of times it called Enter to completely release the object. It is entirely possible that a developer forgets to call Monitor.Exit after he is done with a critical section. For this the framework provides the lock statement. Since the lock statement automatically relinquishes the lock after the block there is nothing to forget!
lock { //Critical section }
So the above lock statement is functionally equivalent to
Monitor.Enter(lockObject) try { //Critical Section } finally { Monitor.Exit(lockObject) }
You might be wondering why the lock information cannot be stored as part of the object itself. After all the CLR does allocate a member to hold the index, so why not use it to hold the state of the lock itself. Well it turns out that the lock is one of the many things that a SyncBlock data structure holds. Other stuff includes the objects hashcode, COM interop information etc.
Moreover, using a separate data structure allows the CLR to move it around in memory to support garbage collection without affecting the object on which the lock is acquired.
You will notice that with this infrastructure it is possible to acquire a lock on ANY reference type including the object you are trying to protect inside the critical section. Compare this with the Win32 CRITICAL_SECTION data structure. To protect a critical section in C++ one would have to explicitly declare a CRITICAL_SECTION object and then call InitializeCriticalSection and EnterCriticalSection on it. (and later, LeaveCriticalSection and DeleteCriticalSection). With this approach, every critical section was associated with an arbitrary critical section.
In .net, since the runtime allocates and maintains the SyncBlocks, the developer does not have to deal with the overhead involved in creating and deleting critical sections which is a good thing. Also, SyncObjects are by their nature lightweight objects as opposed to acquiring locks on a CRITICAL_SECTION object in C++. But the ability to acquire locks on any reference object (as opposed to a dedicated lock object, like the CRITICAL_SECTION object in C++) brings in this rather interesting problem of locking on this. (Explained in this article)
Code safely
Alex