0

I'm trying to understand some basic multitasking approaches and concepts like Thread/Task/Semaphore. I'm a bit confused about semaphore and it's waiting mechanism.

I have created an example program. Fiddle

What I want to do is executing BigLock() function in a certain time as some TestFunctions are running. If there are some TestFunctions are running at the time of calling BigLock() function, it should allow them to finish but not allow new TestFunction execution until BigLock() finishes its job.

public class Program
{   
        public static void Main(string[] args)
        {
            TestClass test = new TestClass();
            test.TestRunner();
            Thread.Sleep(100000);
        }
}

public class TestClass
{
        List<SemaphoreSlim> semaphoreArray = new List<SemaphoreSlim>();
        SemaphoreSlim locker = new SemaphoreSlim(1);
        private async void TestFunction(int i)
        {
            SemaphoreSlim s = new SemaphoreSlim(1);
            semaphoreArray.Add(s);
            await locker.WaitAsync();
            await s.WaitAsync();
            Console.WriteLine("{0} starts waiting",Task.CurrentId);
            Thread.Sleep(1000);
            s.Release();
            locker.Release();
            Console.WriteLine("{0} finished and released semaphore",Task.CurrentId);
        }
        public void TestRunner()
        {
            Task.Run(() => Parallel.For(1, 10, (i, state) => TestFunction(i)));
            Task.Run(()=>BigLock());
            Parallel.For(1, 10, (i, state) => TestFunction(i));
        }
        private async void BigLock()
        {
            Console.WriteLine("Started waiting all semaphores");
            await locker.WaitAsync();
            Console.WriteLine("WE ARE IN");
            WaitHandle[] waitHandles = semaphoreArray
                .Select(x => x.AvailableWaitHandle).ToArray();
            if(waitHandles.Count()>0){
                WaitHandle.WaitAll(waitHandles);
            }
            Console.WriteLine("That's the end");
            locker.Release();
        }
}

I expected that, after completion of some of the TestFunction executions, BigLock continues running.

But the output was not as I expected.

Started waiting all semaphores
WE ARE IN
That's the end
1 starts waiting
1 finished and released semaphore
 starts waiting
 starts waiting
 finished and released semaphore
 finished and released semaphore
 starts waiting
 finished and released semaphore
 starts waiting
 finished and released semaphore
 starts waiting
 finished and released semaphore
 starts waiting
 finished and released semaphore
 starts waiting
 finished and released semaphore
 starts waiting
 finished and released semaphore
 starts waiting 

I added some delay to call BigLock function and run the program again.

public void TestRunner()
{
   Task.Run(() => Parallel.For(1, 10, (i, state) => TestFunction(i)));
   Thread.Sleep(300);
   Task.Run(()=>BigLock());
            
   Parallel.For(1, 10, (i, state) => TestFunction(i));
}

The output was

1 starts waiting
1 finished and released semaphore
 starts waiting
 finished and released semaphore
 starts waiting
Started waiting all semaphores
 finished and released semaphore
 starts waiting
 finished and released semaphore
 starts waiting
 finished and released semaphore
 starts waiting
 finished and released semaphore
 starts waiting
 finished and released semaphore
 starts waiting
 finished and released semaphore
 starts waiting
 finished and released semaphore
 starts waiting
 finished and released semaphore
 starts waiting
 finished and released semaphore
 starts waiting
 finished and released semaphore
 starts waiting
 finished and released semaphore
 starts waiting
 finished and released semaphore
 starts waiting
 finished and released semaphore
 starts waiting
 finished and released semaphore
 starts waiting
 finished and released semaphore
 starts waiting
 finished and released semaphore
WE ARE IN
That's the end

What I understood from the second output is locker SemaphoreSlim started waiting after 2nd call of TestFunction executed and 3rd execution was started (line 6)

After that line I expected BigLock function would continue running after 3rd call of Test execution finished.

But it didn't run until all calls to TestFunction had finished.

Is there a priority mechanism that should be considered for that kind of scenarios?

Theodor Zoulias
  • 34,835
  • 7
  • 69
  • 104
tzairos
  • 35
  • 2
  • 8
  • 3
    I didn't dive deep into your code, because there are lots of red flags on the surface. For starters `async void` [should be avoided](https://learn.microsoft.com/en-us/archive/msdn-magazine/2013/march/async-await-best-practices-in-asynchronous-programming#avoid-async-void) in general, and using more than one lockers makes your solution susceptible to deadlocks. See the [dining philosophers problem](https://en.wikipedia.org/wiki/Dining_philosophers_problem). Regarding the `SemaphoreSlim`, according to the documentation it's not guaranteed to be acquired in FIFO order. – Theodor Zoulias Aug 12 '21 at 18:08
  • @TheodorZoulias thanks for red flag explanations. I'll try to give you more concreate example. You can think the example code as a cache. TestFunction is Add(ing) the data to cache. BigLock function is responsible of flushing the cache. If I want to flush the cache, as some threads are in Add ( assuming adding is not atomic , it's fetching and adding) function; - I want to let running threads finish their execution -I want to not allow any other threads start running Add function until I flush the cache. What is the possible ways of doing that? – tzairos Aug 12 '21 at 18:19
  • 3
    The simplest solution is to have a single locker, and use it everytime a thread (or a task) wants to access the cache, either for reading/writing a value, or for flushing it. – Theodor Zoulias Aug 12 '21 at 18:25
  • 1
    TPL Dataflow provides classes and data structures for buffering, producer/consumer scenarios, ... Maybe https://learn.microsoft.com/en-us/dotnet/standard/parallel-programming/how-to-write-messages-to-and-read-messages-from-a-dataflow-block is what you need. – Michael Aug 12 '21 at 18:33
  • 1
    If this is for academic purposes, two locks when properly implemented can perform well and avoid deadlocks. `ReaderWriterLock` uses two locks for example. If you are interested in toying with multiple locks, consider using an `EventWaitHandle`(and/or it's derived classes) instead of a `Semaphore` you will find it much more easy to design the overall system. – DekuDesu Aug 13 '21 at 02:33

0 Answers0