The sample won't work correctly.
A task is not a thread. Instead, a task is scheduled on a thread by a taskscheduler.
In this sample, 100 tasks are created and run. The default taskscheduler creates 1 thread per CPU, no more. It then schedule each task on a free thread until there is no more task to run.
A task can be broken into 2 "runs" by the "await" keyword. I mean, when the await keyword is encountered, the taskscheduler frees the tread and schedule another task on it.
As the same thread is used for 2 running tasks, the lock keyword won't lock for calls originating from these threads.
Thus defeating its purpose.
⚠Do not edit this section. It is required for docs.microsoft.com ➟ GitHub issue linking.
@softlion
As the same thread is used for 2 running tasks, the lock keyword won't lock for calls originating from these threads.
Could you please elaborate on that?
Also, what makes the sample in the article not work? Your reasoning in the second half of the post is based on await breaking the task in two parts. However, the article example doesn't use await at all.
Note that the docs say that:
You can't use the
awaitkeyword in the body of alockstatement.
Yes, the problem arises when the method RandomlyUpdate is async, and you await inside it (try await Task.Delay(100) for example.
Of course as the method in the example is not async, it works. But it is VERY comfusing as all developers will try to use lock like in the credit/debit method, but may call them from an async method which has switch its context.
This means the lock won't work in all cases. Which leads to random errors in production.
Instead you should point to SemaphoreSlim, explaining that it resolves the problem.
@softlion
Yes, the problem arises when the method RandomlyUpdate is async, and you await inside it (try await Task.Delay(100) for example.
For the sake of the example, let's simplify (call only the Credit method with the known argument, so we know what result to expect) the RandomlyUpdate method to this one (note, it also outputs the ID of the current thread):
static void RandomlyUpdate(Account account)
{
var amount = 1.0m;
for (int i = 0; i < 10; i++)
{
Console.WriteLine(Thread.CurrentThread.ManagedThreadId);
account.Credit(amount);
}
}
As we start with 1000 at the account and launch 100 tasks, expected final result is 2000 (every task calls RandomlyUpdate once; the method adds 1 to the account ten times). Run it:
3
Balance before credit:1991.0
Amount to add : 1.0
Balance after credit :1992.0
6
Balance before credit:1992.0
Amount to add : 1.0
Balance after credit :1993.0
5
Balance before credit:1993.0
Amount to add : 1.0
Balance after credit :1994.0
Balance before credit:1994.0
Amount to add : 1.0
Balance after credit :1995.0
Balance before credit:1995.0
Amount to add : 1.0
Balance after credit :1996.0
Balance before credit:1996.0
Amount to add : 1.0
Balance after credit :1997.0
Balance before credit:1997.0
Amount to add : 1.0
Balance after credit :1998.0
Balance before credit:1998.0
Amount to add : 1.0
Balance after credit :1999.0
Balance before credit:1999.0
Amount to add : 1.0
Balance after credit :2000.0
Press any key to continue . . .
The final result is as expected; we also see that the method is called from multiple threads.
Now, let's try await something:
static async Task RandomlyUpdate(Account account)
{
var amount = 1.0m;
for (int i = 0; i < 10; i++)
{
Console.WriteLine(Thread.CurrentThread.ManagedThreadId);
account.Credit(amount);
await Task.Delay(100);
}
}
The final result is the same:
Balance before credit:1994.0
Amount to add : 1.0
Balance after credit :1995.0
12
Balance before credit:1995.0
Amount to add : 1.0
Balance after credit :1996.0
9
Balance before credit:1996.0
Amount to add : 1.0
Balance after credit :1997.0
8
Balance before credit:1997.0
Amount to add : 1.0
Balance after credit :1998.0
3
Balance before credit:1998.0
Amount to add : 1.0
Balance after credit :1999.0
4
Balance before credit:1999.0
Amount to add : 1.0
Balance after credit :2000.0
Press any key to continue . . .
I understand that one run might be not enough to reproduce the problem. However, what kind of problem should I expect when using await? And what mechanism leads to this problem? Why "the lock keyword won't lock"?
Let's say, that every iteration in this method:
static async Task RandomlyUpdate(Account account)
{
var amount = 1.0m;
for (int i = 0; i < 10; i++)
{
Console.WriteLine(Thread.CurrentThread.ManagedThreadId);
account.Credit(amount);
await Task.Delay(100);
}
}
runs on a different thread. And while the iteration 2 of Task#1 runs on thread#2, the iteration 2 of Task#2 runs on thread#1. How does it influence lock behaviour?
@softlion it looks we're talking about very different things here and I want to understand your point.
I'm going to close this due to lack of response.
Sorry for the delay.
It won't work because tasks are NOT threads.
Tasks are queued and dispatched on max X threads where X is the number of processor minus one.
The lock keyword prevents only a code to run on different threads. Not on the same one.
You forgot to use ConfigureAwait(false) in your await. This is required to simulate a task which returns to the queue while waiting on a resource.
So one task starts, is interrupted and goes back to the queue. Another one starts on the SAME THREAD and completes. Then the interrupted task continues.
The lock keyword did not stop the execution of the second task thus leading to bad behavior.
@softlion That would only happen if you could await inside a lock, but doing that is not allowed exactly for this reason.
If you use a lock before or after an await, then there is no problem.
Also:
Tasks are queued and dispatched on max X threads where X is the number of processor minus one.
That is not true, the ThreadPool will (slowly) create many more threads than that when necessary. For example, on my quad-core, ThreadPool.GetMaxThreads says it will create up to 1023 threads.
Most helpful comment
@softlion
For the sake of the example, let's simplify (call only the
Creditmethod with the known argument, so we know what result to expect) theRandomlyUpdatemethod to this one (note, it also outputs the ID of the current thread):As we start with 1000 at the account and launch 100 tasks, expected final result is 2000 (every task calls
RandomlyUpdateonce; the method adds 1 to the account ten times). Run it:The final result is as expected; we also see that the method is called from multiple threads.
Now, let's try await something:
The final result is the same:
I understand that one run might be not enough to reproduce the problem. However, what kind of problem should I expect when using
await? And what mechanism leads to this problem? Why "the lock keyword won't lock"?Let's say, that every iteration in this method:
runs on a different thread. And while the iteration 2 of Task#1 runs on thread#2, the iteration 2 of Task#2 runs on thread#1. How does it influence
lockbehaviour?@softlion it looks we're talking about very different things here and I want to understand your point.