0

I'm wanting to run a parallel process using a thread-safe singleton for a portion of that process. I'm using COM Interop in the Singleton and can only have one instance running at a time system-wide.

The Singleton's operations cannot overlap. When a Singleton operation is called, there can't be any other singleton operations currently in-process..

I have a simplified, working example that using lock to ensure the singleton operation stay serialized across the multiple threads.

Would anyone recommend a more optimal solution than what I have currently OR is what I have implemented appropriate? Thank you.

Current working example-

internal class Program
{
    static void Main(string[] args)
    {
        Parallel.ForEach(Enumerable.Range(0, 10), i =>
        {
            if (i % 2 == 0)
                SerializedSingleton.Instance.Serializer(); // Serialized Operation Only
            else 
                Thread.Sleep(100); // Concurrent Operations
        });
    }
}

public sealed class SerializedSingleton
{
    private static readonly object SerializerLock = new object();
    // THREAD-SAFE SINGLETON SETUP
    private static readonly Lazy<SerializedSingleton> lazy =
    new Lazy<SerializedSingleton>(() => new SerializedSingleton());
    public static SerializedSingleton Instance { get => lazy.Value; }
    private SerializedSingleton()
    {
    }
    public void Serializer()
    {
        lock (SerializerLock) 
        {
            Console.WriteLine("Serializer START");
            Thread.Sleep(2000);
            Console.WriteLine("Serializer STOP");
        }
    }
}

Current Output (desired)-

Serializer START
Serializer STOP
Serializer START
Serializer STOP
Serializer START
Serializer STOP
Serializer START
Serializer STOP
Serializer START
Serializer STOP

Need to avoid-

Serializer START
Serializer START
Serializer START
Serializer START
Serializer START
Serializer STOP
Serializer STOP
Serializer STOP
Serializer STOP
Serializer STOP

Thead-safe singleton sourced from: https://csharpindepth.com/Articles/Singleton

9krausec
  • 3
  • 1
  • *"can only have one instance running at a time system-wide."* -- What do you mean with system-wide? Are you referring to the current process, or to all processes running on the same machine. – Theodor Zoulias Feb 10 '22 at 22:17
  • @TheodorZoulias - This is for a console application meant to run as a utility on a workstation. The .NET Framework console app hooks into a pre-existing software instance running on the computer or startup a headless instance of the software in the background. The software (Solidworks) can have one instance running on the machine for stability reasons. Sadly, this Solidworks COM manipulation is the only component of the console app that does not support parallelization. Any Solidworks COM operation needs to be serial cross the board. Solidworks is also only conditionally used in the app. – 9krausec Feb 11 '22 at 00:09
  • 1
    So you need a cross-process lock. The `lock` statement is not cross-process. To create a machine-wide protected region (cooperatively), you need a *named* `Mutex` or `Semaphore`. If you want it also to be asynchronous, you can take a look at [this](https://stackoverflow.com/questions/69354109/async-friendly-cross-process-read-write-lock-net) question. – Theodor Zoulias Feb 11 '22 at 00:24
  • The application could try acquire a named mutex on startup to make sure it is the only instance running, and then use whatever synchronization primitives suit it best within that instance – Andrew Williamson Feb 11 '22 at 01:11
  • 1
    @TheodorZoulias - I'll look into that solution and explore it. This console app will be used in a controlled environment among a small internal team. I was planning to train them to only fire this app once at a time. I was however planning to check the ROT before executing and terminate if another of the same process was found to be running... That would be lazy though and you've inspired me to explore this potential of cross-process locking to make things much more flexible. I find all of this to be very interesting. Thank you for pointing me in the right direction on all of this! – 9krausec Feb 11 '22 at 01:15

1 Answers1

2

Whenever you call lock (SerializerLock) and Thread.Sleep(..), you block the current thread. If there is high contention, you will have a lot of threads waiting for the lock. That has a little impact on your CPU, and reduces the number of threads available in the thread pool. An alternative would be to use SemaphoreSlim, and make the locking asynchronous:

public sealed class SerializedSingleton
{
    // This doesn't need to be static if the instance is a singleton
    private readonly SemaphoreSlim serializerLock = new SemaphoreSlim(1, 1);

    private static readonly Lazy<SerializedSingleton> lazy =
        new Lazy<SerializedSingleton>(() => new SerializedSingleton());

    public static SerializedSingleton Instance { get => lazy.Value; }

    public async Task SerializerAsync()
    {
        await serializerLock.WaitAsync();

        try
        {
            Console.WriteLine("Serializer START");
            await Task.Delay(2000);
            Console.WriteLine("Serializer STOP");
        }
        finally
        {
            serializerLock.Release();
        }
    }
}
Andrew Williamson
  • 8,299
  • 3
  • 34
  • 62
  • Awesome! This seems to be working great. I'll do some reading into SemaphoreSlim to better understand the concept. In the Parallel.ForEach loop I had to make sure to wrap the await up in a new Task, and everything seems to be working slick. ``` – 9krausec Feb 11 '22 at 00:58