Question

Jeffrey Richter pointed out in his book 'CLR via C#' the example of a possible deadlock I don't understand (page 702, bordered paragraph).

The example is a thread that runs Task and call Wait() for this Task. If the Task is not started it should possible that the Wait() call is not blocking, instead it's running the not started Task. If a lock is entered before the Wait() call and the Task also try to enter this lock can result in a deadlock.

But the locks are entered in the same thread, should this end up in a deadlock scenario?

The following code produce the expected output.

class Program
{
    static object lockObj = new object();

    static void Main(string[] args)
    {
        Task.Run(() =>
        {
            Console.WriteLine("Program starts running on thread {0}",
                Thread.CurrentThread.ManagedThreadId);
            var taskToRun = new Task(() =>
            {
                lock (lockObj)
                {
                    for (int i = 0; i < 10; i++)
                        Console.WriteLine("{0} from Thread {1}", 
                            i, Thread.CurrentThread.ManagedThreadId);
                }
            });

            taskToRun.Start();
            lock (lockObj)
            {
                taskToRun.Wait();
            }

        }).Wait() ;
    }
}

/* Console output
Program starts running on thread 3
0 from Thread 3
1 from Thread 3
2 from Thread 3
3 from Thread 3
4 from Thread 3
5 from Thread 3
6 from Thread 3
7 from Thread 3
8 from Thread 3
9 from Thread 3
*/

No deadlock occured.

J. Richter wrote in his book "CLR via C#" 4th Edition on page 702:

When a thread calls the Wait method, the system checks if the Task that the thread is waiting for has started executing. If it has, then the thread calling Wait will block until the Task has completed running. But if the Task has not started executing yet, then the system may (depending on the TaskScheduler) execute the Trask by using the thread that called Wait. If this happens, then the thread calling Wait does not block; it executes the Task and returns immediatlely. This is good in that no thread has blocked, thereby reducing resource usage (by not creating a thread to replace the blocked thread) while improving performance (no time is spet to create a thread an there is no contexte switcing). But it can also be bad if, for example, thre thread has taken a thread synchronization lock before calling Wait and thren the Task tries to take the same lock, resulting in a deadlocked thread!

If I'm understand the paragraph correctly, the code above has to end in a deadlock!?

Was it helpful?

Solution

You're taking my usage of the word "lock" too literally. The C# "lock" statement (which my book discourages the use of), internally leverages Monitor.Enter/Exit. The Monitor lock is a lock that supports thread ownership & recursion. Therefore, a single thread can acquire this kind of lock multiple times successfully. But, if you use a different kind of lock, like a Semaphore(Slim), an AutoResetEvent(Slim) or a ReaderWriterLockSlim (without recursion), then when a single thread tries to acquire any of these locks multiple times, deadlock occurs.

OTHER TIPS

In this example, you're dealing with task inlining, a not-so-rare behavior of the TPL's default task scheduler. It results in the task being executed on the same thread which is already waiting for it with Task.Wait(), rather than on a random pool thread. In which case, there is no deadlock.

Change your code like below and you'll have a dealock:

taskToRun.Start();
lock (lockObj)
{
    //taskToRun.Wait();

    ((IAsyncResult)taskToRun).AsyncWaitHandle.WaitOne();
}

The task inlining is nondeterministic, it may or may not happen. You should make no assumptions. Check Task.Wait and “Inlining” by Stephen Toub for more details.

Updated, the lock does not affect the task inlining here. Your code still runs without deadlock if you move taskToRun.Start() inside the lock:

lock (lockObj)
{
    taskToRun.Start();
    taskToRun.Wait();
}

What does cause the inlining here is the circumstance that the main thread is calling taskToRun.Wait() right after taskToRun.Start(). Here's what happens behind the scene:

  1. taskToRun.Start() queues the task for execution by the task scheduler, but it hasn't been allocated a pool thread yet.
  2. On the same thread, the TPL code inside taskToRun.Wait() checks if the task has already been allocated a pool thread (it hasn't) and executes it inline on the main thread. In which case, it's OK to acquired the same lock twice without a deadlock.
  3. There is also a TPL Task Scheduler thread. If this thread gets a chance to execute before taskToRun.Wait() is called on the main thread, inlining doesn't happen and you get a deadlock. Adding Thread.Sleep(100) before Task.Wait() would be modelling this scenario. Inlining also doesn't happen if you don't use Task.Wait() and rather use something like AsyncWaitHandle.WaitOne() above.

As to the quote you've added to your question, it depends on how you read it. One thing is for sure: the same lock from the main thread can be entered inside the task, when the task gets inlined, without a deadlock. You just cannot make any assumptions that it will get inlined.

In your example, no deadlock occurs because the thread scheduling the task and the thread executing the task happen to be the same. If you were to modify the code such that your task ran on a different thread, you would see the deadlock occur, because two threads would then be contending for a lock on the same object.

Your example, modified to create a deadlock:

class Program {
    static object lockObj = new object();

    static void Main(string[] args) {
        Console.WriteLine("Program starts running on thread {0}",
            Thread.CurrentThread.ManagedThreadId);
        var taskToRun = new Task(() => {
            lock (lockObj) {
                for (int i = 0; i < 10; i++)
                    Console.WriteLine("{0} from Thread {1}",
                        i, Thread.CurrentThread.ManagedThreadId);
            }
        });

        lock (lockObj) {
            taskToRun.Start();
            taskToRun.Wait();
        }
    }
}

This example code has two standard threading problems. To understand it, you first have to understand thread races. When you start a thread, you can never assume it will start running right away. Nor can you assume that the code inside the thread arrives at a particular statement at a particular moment in time.

What matters a great deal here is whether or not the task arrives at the lock statement before the main thread does. In other words, whether it races ahead of the code in the main thread. Do model this as a horse race, the thread that acquired the lock is the horse that wins.

If it is the task that wins, pretty common on modern machines with multiple processor cores or a simple program that doesn't have any other threads active (and probably when you test the code) then nothing goes wrong. It acquires the lock and prevents the main thread from doing the same when it, later, arrives at the lock statement. So you'll see the console output, the task finishes, the main thread now acquires the lock and the Wait() call quickly completes.

But if the thread pool is already busy with other threads, or the machine is busy executing threads in other programs, or you are unlucky and you get an email just as the task starts running, then the code in the task doesn't start running right away and it is the main thread that acquired the lock first. The task can now no longer enter the lock statement so it cannot complete. And the main thread can not complete, Wait() will never return. A deadly embrace called deadlock.

Deadlock is relatively easy to debug, you've got all the time in the world to attach a debugger and look at the active threads to see why they are blocked. Threading race bugs are incredibly difficult to debug, they happen too infrequently and it can be very difficult to reason through the ordering problem that causes them. A common approach to diagnose thread races is to add tracing to the program so you can see the order. Which changes the timing and can make the bug disappear. Lots of programs were shipped with the tracing left on because they couldn't diagnose the problem :)

Thanks @jeffrey-richter for pointing it out, @embee there are scenario when we use locks other than Monitor than a single thread tries to acquire any of these locks multiple times, deadlock occurs. Check out the example below

The following code produce the expected deadlock. It need not be nested task the deadlock can occur without nesting also

class Program
{
    static AutoResetEvent signalEvent = new AutoResetEvent(false);

    static void Main(string[] args)
    {
        Task.Run(() =>
        {
            Console.WriteLine("Program starts running on thread {0}",
                 Thread.CurrentThread.ManagedThreadId);
            var taskToRun = new Task(() =>
            {
                signalEvent.WaitOne();
                for (int i = 0; i < 10; i++)
                    Console.WriteLine("{0} from Thread {1}", 
                        i, Thread.CurrentThread.ManagedThreadId);
            });

            taskToRun.Start();
            signalEvent.Set();
            taskToRun.Wait();

        }).Wait() ;
    }
}
Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top