Reputation: 13
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
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
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