.NET Synchronisation APIs - Part 2 - Out-of-Process Synchronisation
Introduction
This is the second in a series of posts on .NET synchronisation:
- In-process synchronisation
- Out-of-process synchronisation on the same machine (this post)
- Distributed synchronisation
This time I'm going to talk about using the synchronisation APIs I described on the first post, dedicated to in-process synchronisation, but in out-of-process context, meaning, to synchronise different processes, not threads. This can be achieved out of the box with three synchronisation objects:
- Mutex
- Semaphore
- EventWaitHandle (remember, parent class of AutoResetEvent and ManualResetEvent)
These objects follow the same pattern: open (fails if doesn't exist) or try to open an existing object by its name. For a synchronisation object to be available system-wide it needs a name and can have fine-grained permissions on Windows systems. I will cover permissions more to the end of this post.
I will also be covering an additional technique that can also be used for synchronisation: shared files.
Mutex
Why would we have a cross-process Mutex? Well, imagine for example that you want an application to only have at most one executing instance; you can achieve that with a mutex.
Here is a simple example using OpenExisting:Mutex? mutex = null;
var mutexName = "Global\\My Application";
try
{
mutex = Mutex.OpenExisting(mutexName);
if (!mutex.WaitOne(TimeSpan.FromSeconds(0))
{
//mutex is currently locked, meaning, another instance of the app is running
}
}
catch (WaitHandleCannotBeOpenedException)
{
//does not exist
}
catch (UnauthorizedAccessException ex)
{
//no permission to access it
}Semaphore
For Semaphore, it is very mich the same as for Mutex, except multiple processes can get inside. Think, for example, of a number of worker processes you want to allow in. Here is an example using TryOpenExisting and WaitOne with a timeout:
Semaphore? semaphore = null;
var semaphoreName = "FooBar";
if (Semaphore.TryOpenExisting(semaphoreName, out semaphore))
{
if (!semaphore.WaitOne(TimeSpan.FromSeconds(0))
{
//semaphore is currently locked, need to wait until someone releases it
}
}
else
{
semaphore = new Semaphore(0, 3, semaphoreName);
}EventWaitHandle
You may recall from the previous post that EventWaitHandle is the parent class of both AutoResetEvent and ManualResetEvent. When we ask for a named event object, we do not know it's type, it can either be an AutoResetEvent or a ManualResetEvent, and we should not be bothered with that.
EventWaitHandle? evt = null;
var eventName = "FooBar";
if (EventWaitHandle.TryOpenExisting(eventName, out evt))
{
if (!evt.WaitOne(TimeSpan.FromSeconds(0))
{
//event is currently locked, need to wait until someone signals it
}
}As you can see, EventWaitHandle, Semaphore, and Mutex all follow the same pattern: OpenExisting/TryOpenExisting or create with a name.
Shared Files
Using a shared file for synchronising accross processes (even on different machines, if the file is located in shared storage) is yet another option.
File.Open lets us open an existing file with different access and share modes. If we use FileShare.None, then the file is either open with exclusive access or any attempt to open it fails, until the file is closed by the opening process:
FileStream file = null;
var name = "\\file.lock";
try
{
file = File.Open(name, FileMode.Create, FileAccess.ReadWrite, FileShare.None);
//perform critical operation
file.Dispose(); //release lock
}
catch
{
//file could not be open exclusively
}
As you can see, pretty straightforward, but with one caveat: the file stays created, even if a crash occurs, so you must be aware of stale files! Files can of course also be created with fine-grained security (access control lists) but I will not cover it here.
Sharing Data
What if you want to share data between the processes that you are synchronising? There are a couple options like:
I won't dwell on this, but I once wrote a post on IPC on the same machine, if you're curious, you can have a look, even though it is somewhat outdated.
Security
Namespace and Scope
EventWaitHandle, Semaphore and Mutex can take a NamedWaitHandleOptions parameter on their constructors which offers two boolean properties:
- CurrentSessionOnly: visible only within the the current session
- CurrentUserOnly: limited in access to the current user
This defines the scope for the object. Alternatively, we can control names passed to EventWaitHandle, Semaphore and Mutex's constructors; these must fall into two possible namespaces:
- Local\<name>: visible only within the current session (normal desktop app behaviour)
- Global\<name>: visible across all sessions
As an alternative to the methods that accept a NamedWaitHandleOptions parameter to set the scope, methods that only take a name parameter can prefix it with "Global\" or "Local\" to specify a namespace. When the Global namespace is specified, the synchronisation object can be shared with any processes on the system; when Local is specified (the default when no namespace is specified), the synchronisation object can be shared with processes in the same session.
Access Control Lists
A different option is to control who can use your synchronisation object using Access Control Lists (ACLs): an ACL contains the intended users/groups (security identifiers, or SIDs) for the synchronisation object, rights/permissions on the it (modify, delete, full control, etc), and the access control type (access, deny). This is really only valid for Windows. To use ACLs, this is what we need to do:
var allowedUser = "...";
// can be in the following formats:
// "DOMAIN\User" -> domain user/group
// ".\LocalUser" -> local user/group
// "S-1-5-21-...." -> SID that describes any user/group
// Windows: get the security id (SID) for a named user
var userSid = (SecurityIdentifier) new NTAccount(allowedUser)
.Translate(typeof(SecurityIdentifier));
var security = new SemaphoreSecurity();
// explicitly allow this user
security.AddAccessRule(new SemaphoreAccessRule(userSid, SemaphoreRights.Synchronize | SemaphoreRights.Modify, AccessControlType.Allow));
// often wise to also allow LocalSystem (for services, OS components), optional using WellKnownSidType.LocalSystemSid SID:
security.AddAccessRule(new SemaphoreAccessRule(new SecurityIdentifier(WellKnownSidType.LocalSystemSid, null), SemaphoreRights.FullControl, AccessControlType.Allow));
ver semaphore = new Semaphore(1, 3, "Global\\semaphore_name");
semaphore.SetAccessControl(security);By default, when we apply security, all users/groups are denied, so we need to explicitly allow the users/groups we want to allow, and what is their level of access. The former example is for Semaphore but it is essentially the same for Mutex and EventWaitHandle. Here are the types involved:
| Synchronisation Object | Access Rules | Rules Gathering | ACL Extensions | Rights |
| Semaphore | SemaphoreAccessRule | SemaphoreSecurity | SemaphoreAcl | SemaphoreRights |
| Mutex | MutexAccessRule | MutexSecurity | MutexAcl | MutexRights |
| EventWaitHandle | EventWaitHandleAccessRule | EventWaitHandleSecurity | EventWaitHandleAcl | EventWaitHandleRights |
And this is a brief explanation, somewhat simplified:
- Rules Gathering: collects all the access rules for a synchronisation object. It's a xxxSecurity class
- Access Rules: consists of a security identifier (SID) and a set of rights with an access control type. A xxxAccessRule class
- ACL Extensions: extension methods for working (creating, opening existing) with synchronisation objects and security (Create, OpenExisting, TryOpenExisting). Exist on a xxxAcl class
- Rights: the actual rights (permissions) on the target object for a given SID (Modify, Delete, ReadPermissions, ChangePermissions, TakeOwnership, Synchronize, and FullControl). Exist as xxxRights enumerations
- Access Control Type: one of Allow, Deny values from the AccessControlType enumeration, for all synchronisation objects
The actual attachment of security to an object is achieved through the SetAccessControl/GetAccessControl extension methods from the ThreadingAclExtensions class, that apply to both Semaphore, Mutex, and EventWaitHandle. This requires the System.Threading.AccessControl NuGet package.
Conclusion
Named synchronisation objects are an essential tool for doing any advanced desktop (non-web) development, such as Windows Forms, WPF, Maui, or even console or Windows Services apps. You should pick the one that best suits your needs, and here is a simple guide for selecting the right synchronisation object:
- When you need do synchronise on the same machine, just need mutual exclusion: named Mutex
- Allow up to N concurrent holders: named Semaphore
- Just need “wake up/signal” between processes: named EventWaitHandle; it can release exactly one waiting process (AutoResetEvent) or all (ManualResetEvent)
- Access across shared storage, survive crashes (beware of stale locks!): shared files
Don't forget that fine-grained security only works on Windows.
Comments
Post a Comment