ASP.NET Core 6 - Strange Monitor.TryLock behavior

Hi, I'm making some experiment to better understand the Monitor. Looks like something is off: I've written the following code
internal class Program
{
private static Object obj = new();

static async Task Main(string[] args) {
for (int i = 0; i < 9; i++) {
Console.WriteLine(string.Concat("Starting task ", i));
var task = Task.Run(() => Foo(i));
await Task.Delay(500);
}
}

private static async Task Foo(int value) {
if (Monitor.TryEnter(obj)) {
Console.WriteLine(value + ": monitor entered");
await Task.Delay(new Random().Next() % 2 * 1000);
Monitor.Exit(obj);
Console.WriteLine(value + ": monitor released");
} else {
Console.WriteLine(value + ": monitor refused me "));
}
}
}
internal class Program
{
private static Object obj = new();

static async Task Main(string[] args) {
for (int i = 0; i < 9; i++) {
Console.WriteLine(string.Concat("Starting task ", i));
var task = Task.Run(() => Foo(i));
await Task.Delay(500);
}
}

private static async Task Foo(int value) {
if (Monitor.TryEnter(obj)) {
Console.WriteLine(value + ": monitor entered");
await Task.Delay(new Random().Next() % 2 * 1000);
Monitor.Exit(obj);
Console.WriteLine(value + ": monitor released");
} else {
Console.WriteLine(value + ": monitor refused me "));
}
}
}
And I have this strange (apparently) result:
Starting task 0
0: monitor entered
Starting task 1
1: monitor refused me :c
Starting task 2
2: monitor entered
Starting task 3
3: monitor refused me :c
2: monitor released
Starting task 4
4: monitor entered
Starting task 5
5: monitor refused me :c
Starting task 6
6: monitor refused me :c
Starting task 7
7: monitor refused me :c
Starting task 8
8: monitor entered
8: monitor released
Starting task 0
0: monitor entered
Starting task 1
1: monitor refused me :c
Starting task 2
2: monitor entered
Starting task 3
3: monitor refused me :c
2: monitor released
Starting task 4
4: monitor entered
Starting task 5
5: monitor refused me :c
Starting task 6
6: monitor refused me :c
Starting task 7
7: monitor refused me :c
Starting task 8
8: monitor entered
8: monitor released
At first I couldn't understand this strange behavior. Now I've noticed that Monitor.Try enter description says "returns true if the current thread acquires the lock". From my understanding, Task.Run may or may not create a new thread, so for example task 0 and task 2 could have been executed inside the same thread, and this would explain why task 2 entered the monitor even though it should have been acquired by task 0. What do you think? is this the correct explanation? If that's the case, it seems to me Monitor is not suitable for protect a resource from multiple-task access
3 Replies
Tvde1
Tvde12y ago
I think your explanation might be correct. You can try also logging the Thread.CurrentThread.ManagedThreadId to confirm it is the same thread. By doing await Task.Delay(), .NET frees up the current thread and just makes it so any thread will pick up the work when the delay is completed. Also depending on the TaskScheduler and Synchronization Contexts of the current application, the thread that continues after the Task.Delay might not be the same thread that aquired the monitor (so a different thread might exit the Monitor) but I'm not sure if that's the case here I think that would throw exceptions
Klarth
Klarth2y ago
Yeah, looks to be an issue with mixing Monitor and async. The top two answers here are good: https://stackoverflow.com/questions/21410320/monitor-tryenter-doesnt-work
alkasel#159
alkasel#1592y ago
I didn't though about the problem "the thread that continues after the Task.Delay might not be the same thread that aquired the monitor", that would explain why in the previous version of the code, where I had a try-catch block, sometimes it happened that task number x would acquire the lock and, after sleeping, it could not acquire the lock. I'm gonna check again since I don't remember precisely Thanks