CoordinatedFool
CoordinatedFool

Reputation: 13

How to best handle multiple tasks accessing the same mutex?

So, I have multiple tasks that lock/release the same mutex. After reading this:

While a mutual-exclusion lock is held, code executing in the same execution thread can also obtain and release the lock. However, code executing in other threads is blocked from obtaining the lock until the lock is released. 1

I got a bit nervous. This should mean two tasks can access the same mutex if they're scheduled on the same thread?

For context, I will have one long-running task (could be replaced by a thread) and multiple small tasks that access the same mutex.

To clarify, this is what I'm worried about:

Thread 0:

 Run Task 1: Lock mutex, do some work

 Pause Task 1

 Run Task 2: Lock mutex, do some work, release mutex

 Run Task 1: keep doing work, release mutex

Upvotes: 1

Views: 1082

Answers (2)

JonasH
JonasH

Reputation: 36541

This should be fine unless you are using async/await inside the critical section. This is because no other task can run on the thread until it completes execution of its current task.

If you are using awaits inside critical sections there can be a problem since "await" may complete the execution as far as the thread is concerned. The simple solution would be to not use await inside a critical section, and this is probably a good idea in general. If you have some specific problem with this it might be a good idea to post it as a new question.

Upvotes: 4

Theodor Zoulias
Theodor Zoulias

Reputation: 43758

So you are concerned that the program below would be buggy, and would not report the correct final value 1,000,000 for the counter:

using System;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;

public class Program
{
    public static void Main()
    {
        const int TASKS_COUNT = 1000;
        const int LOOPS_COUNT = 1000;
        ThreadPool.SetMinThreads(100, 10); // Ensure that we have more threads than cores
        var locker = new object();
        var counter = 0;
        var tasks = Enumerable.Range(1, TASKS_COUNT).Select(async x =>
        {
            for (int i = 0; i < LOOPS_COUNT; i++)
            {
                await Task.Yield();
                lock (locker)
                {
                    counter++;
                }
            }
        }).ToArray();
        Task.WaitAll(tasks);
        Console.WriteLine($"Counter: {counter:#,0}");
    }
}

Output:

Counter: 1,000,000

The reason that this program is correct is because a ThreadPool thread that is interrupted by the operating system in the middle of calculating the non-atomic counter++ line, it will resume with the same calculation when it receives its next time-slice from the operating system. The TaskScheduler will not schedule another task to run in the same thread, before the previous task running on that thread has been completed.

It's worth noting that from the point of view of the TaskScheduler, each code path between two await operators constitutes a separate mini Task. The splitting is done be the async state machine. In the example program above 1,000 tasks are created explicitly, but the actual number of tasks created by the async state machine in total is 1,000,000. All these tasks were scheduled through the TaskScheduler.Current.QueueTask method.

Upvotes: 0

Related Questions