Why don’t critical sections work cross process?

I could have sworn this was answered in a previous blog by someone else (Raymond, Eric Lippert, etc), but…

Someone sent me feedback asking:

Q> Why can’t critical section objects be used across processes compared to mutexes?

Originally, I thought “Man, that’s a silly question, it’s obvious”.

But then I realized that it’s not obvious, because critical sections are different from every other external synchronization mechanism in Windows (there are some internal synchronization mechanisms that share characteristics with critical sections, but they’re not public).

You see, a critical section isn’t a native object type in Windows.  All the other synchronization primitives (mutexes, events, semaphores, etc) are native objects – the user mode semaphore (or mutex, or event) is an object maintained by NT’s object manager.  As such, it has an ACL, a name, all the things that go with being a native object. 

And since these synchronization primitives are maintained by the object manager, they can be shared across processes – another process can open a named handle, or you can dup the handle into another process, or you can have the process be inherited by a child process.

But critical sections are special.

You see, the flexibility that you get by being maintained by the NT object manager has a cost associated with it – every operation that’s performed on the semaphore/mutex/event requires a user mode  to kernel mode transition, as does waiting on the object.

Sometimes that cost is too high – there’s a need for a highly performant lock structure that can be used to protect a region of code.  That’s where the critical section comes into play.

A critical section is just  a structure, it contains a whole bunch of opaque fields.  Inside the EnterCriticalSection routine is code that uses interlocked instructions to acquire the structure without entering the kernel – it’s what makes the critical section so fast.

Of course, the fact that the critical section is just a structure is also why it can’t be shared between processes – since it’s just a chunk of memory that’s in the processes address space, it’s not accessible to other processes.

The clever observer now realizes that this that begs the question: What happens if I initialize a critical section in a shared memory region – after all, it’s just a chunk of memory, I can share a memory region between two processes, and just initialize a critical section in the shared memory region.

This might actually work, for a while.  But the thing about critical sections is that they’re more than just a spin lock.  There’s also a semaphore that’s acquired when the critical section has contention.  And that semaphore isn’t shared between processes (actually, the semaphore isn’t even “allocated” until there’s contention (it’s not allocated, per se)).  If that wasn’t enough, there are also fields within the critical section that point to other external data structures as well – those structures won’t exist in the process that didn’t initialize the critical section.  There’s no way of knowing what will happen if the other process enters the critical section.  If you’re lucky, the process will crash.  If you’re not, you might “just” corrupt memory.

This is a really long answer to a really short question, but sometimes its worth digging into it a bit.

Comments (12)

  1. Anonymous says:

    You should watch that ‘might "just" corrupt memory’, in a short you might find nasties using it to gain kernel level access – I’m thinking two guest processes, trying to race conditional a critical section in shared memory. You probably need to know more about how critical sections are constructed though 🙁

  2. Sorry, I missed the <sarcasm> tag there 🙂

    You’re totally right – it could be horrible.

  3. The short answer to the question is "Because if it did, it’d be called a ‘mutex’."

  4. Anonymous says:

    > I could have sworn this was answered in a previous blog by someone else…

    IIRC, Matt Pietrek wrote a couple of columns about them in his former MSJ column.

  5. Anonymous says:

    Since critical sections are non-kernel objects, it is possible to implemenet them yourself – using shared memory and a shared (named) event. In "Programming Applications for MS Windows", Jeffery Richter came up with a cross-process critical section-like synchronization mechanism, which he calls Optex. A Windows critical section lazily allocates an event kernel object if it has to deal with contention, but the Optex eagerly allocates one on initialization.

  6. Anonymous says:

    > I could have sworn this was answered in a previous blog by someone else (Raymond, Eric Lippert, etc), but…

    Wasn’t me. I’m far from an expert on these sorts of concurrency issues. Interesting post!

  7. anon – the problem with that solution (a kernel object per critsec) doesn’t scale – to be truly lightweight, you need to be able to have thousands and thousands of critical sections in a process.

    While you can have thousands and thousands of kernel objects, there’s a cost associated with them, and it can become prohibitive.

  8. Anonymous says:

    Anon, why wouldn’t you use mutex instead of using an event and an additional synchronization code? Once you create a kernel synchronization object (a named event in the case you describe) you just lost the advantage a critical section gives you (not going to the kernel) so you may just use a kernel synchronization object to do your synchronization.

  9. Anonymous says:

    I didn’t propose to replace Windows critical sections by Optex objects; I just pointed out their existence.

    I doubt someone will need thousands and thousands of these shared between processes, and the alternatives have similar costs anyway.

  10. Anonymous says:

    Jerry: The advantage is still there. Basically you spin for some time, and only then wait on the kernel mode object. If there’s no contention, there’s no wait. You can read about it in the book mentioned.

  11. Anonymous says:

    Cross-process synchronization with locks protecting some shared state is generally non-robust. People who use it risk a cascade of failures once a single process in a synchronization chain fails. (One process dies while holding the lock, the others must somehow come to terms with the inconsistent state that results).

    Unlike a critical section, mutexes report when they’re being "abandoned". Perhaps a very clever use of this feature could be made to somehow keep the other process continue running. If somebody actually did this, I’d be interested to hear.

  12. Anonymous says:

    Incidentally, while your usage of "begs the question" may be gaining in popularity, its original meaning is quite different than most people assume: