0

In C#, how do I handle critical section with two different "rights of way"?

Theoretical use case: imagine a swimming pool (the resource). Many individual swimmers (worker threads A, B, C, and D) can swim into the pool as they feel like to, and its totally OK for multiple swimmers to be in the pool concurrently. However, from time to time there is a private swimming class (another worker thread, E), and the class has rights above all other swimmers. When a class starts, all other swimmers are asked to complete their task (and exit the pool ASAP).

Swimmers who were asked to leave the pool have already taken their shower and left the premise, as technically, they had already completed their task. Any individual swimmer who arrived after the class started must sit on the bench and wait until the class is over before they can swim.

--

Dealing with concurrency I am used to the lock statement but in this case this would result in only one swimmer (thread) at any time, which is not acceptable.

2
  • What is the goal here? Do you want to maximize resource usage on your machine, or do you want E to have limitless potential resource usage, i.e. whatever it needs, it will be given? Would you rather waste some resources that you reserved for E and E ended up not using, or would you rather use your resources but potentially deny E a resource that is already being used when it requests it?
    – Flater
    Nov 9, 2021 at 9:31
  • I’m voting to close this question because OP wrote an answer stating that he had asked the wrong question.
    – John Wu
    Nov 10, 2021 at 3:30

3 Answers 3

7

You're looking for a shared-read/exclusive-write type lock. C# provides that with the ReaderWriterLockSlim Class. The individual swimmers correspond to threads that have read access, while the swimming class would request exclusive write access.

Note that this kind of lock is available in many libraries and languages, so this is not specifically a C# answer, although the referenced class belongs to C# or more precisely the .NET framework.

5
  • Thanks for the link. After reading the dodo I am not convinced this is the right tool for the job. In the case above, both the swimmers and the private class are technically doing "read & write" operations. Think of it as both using a swim() method. I know that there is no critical section for individual swimmers, because I have designed it in a way that they are allocated their own independent swimming lane. But, if both a private class and individual swimmer swim at the same time, they could be swimming the same lane (and collide).
    – Yann
    Nov 9, 2021 at 9:25
  • 5
    The ReaderWriterLockSlim does not care whether you're technically doing read or write operations, it just makes sure that either any number of "readers" or just one writer are holding the lock securing resource access. Whether the threads actually read or write or swim() isn't the class's business. Nov 9, 2021 at 9:28
  • 1
    If you essentially use single swimming lanes, you might use a traditional lock on each lane, with the class locking all available lanes in order. The ReaderWriterLockSlim approach is useful when there can be an arbitrary number of readers accessing a single resource. Nov 9, 2021 at 9:34
  • 2
    @Yann also don't forget the KISS approach: just allocate resources in any obvious way, then put a lock around the resource allocation code (but not the resource using code). So everyone has to go to the front desk before swimming and there is only one front desk.
    – user253751
    Nov 9, 2021 at 19:34
  • 1
    @user253751 Spot on. And that is what I ended up doing: 1) all threads add the task to a queue (e.g. List<Task>), 2) when adding tasks the thread ensure there is no duplicate task (avoids critical section) 3) a separate thread (the "front desk") is responsible for running all tasks in any desired order. The front desk can also further optimise, for example, if the private class does not need all the swimming lanes, then it is possible for private swimmers to join the pool while a class is running.
    – Yann
    Nov 10, 2021 at 0:24
2

So you can use SemaphoreSlim to make a lock with an arbitrary number of concurrent threads.

https://docs.microsoft.com/en-us/dotnet/api/system.threading.semaphoreslim?view=net-5.0

But your swimming class logic is going to need some custom code. I would add a second SwimClass Semaphore with just a single thread, a flag isSwimClassInProgress which is set when a swim class comes along.

Swim(swimmer s)
{

    if(IsSwimClassInProgress)
    {
        await SwimClass.Wait(); //wait for class to end
    }
    await SwimLane.Wait() //wait for free lane and then take it (up to X allowed)
    if(s.IsSwimClass)
    {
        await SwimClass.Wait();
        IsSwimClassInProgress = true; //new swimmers will wait for end of swim class
    }
    //spin up a swimmer thread to do stuff
    if(s.IsSwimClass)
    {
        await SwimClass.Release(); //allow swimmers back in the pool
        IsSwimClassInProgress = false; //needs to be made thread safe
    }
    SwimLane.Release(); //release your lane
}

Not perfect but demonstrates the idea

0

I did not formulate the question correctly.

For instance, It turns out I did not need to lock a "resource" (the pool, or a swimming lane), but rather needed to prevent swimmers to collide by essentially swimming at the same spot, at the same time. For my particular need, it is OK to deny a second swimmer's access, if I know another swimmer is already doing the work I expect it to do.

I found the best way to do this is to register a queue of tasks, as hinted by @user253751:

private Dictionary<Guid, Task> _ordersTasks;

Adding a task is the responsibility of a front-desk. Tasks will not be added to the queue if there is already a duplicate. A task is considered duplicate if the key (Guid) of the registered task is already in the dictionary.

public void AddTaskToQueueIfNotExists(Guid uuid, Task task)
{
    Task existingTask;

    var taskAlreadyExists = _ordersTasks.TryGetValue(uuid, out existingTask);

    if (taskAlreadyExists)
    {
        // Must allow task to rerun if a previous task was not completed successfully.
        if (existingTask.IsCompletedSuccessfully)
        {
            // Do not add the task.
            return;
        }
        else
        {
            // Previous task failed. Remove old task and replace it to rerun
            _ordersTasks.Remove(uuid);
        }
    }

    _ordersTasks.Add(uuid, task);
}

In this basic implementation, the tasks get started as soon as they are added to the queue using _ordersTasks.Add(uuid, task);. But, it would be totally possible to add more control, eg. delaying execution of a task, or allocating tasks to a number of threads (the "swimming lanes") etc.

Not the answer you're looking for? Browse other questions tagged or ask your own question.