5

I'm facing a deadlock-issue in a piece of code of mine. Thankfully, I've been able to reproduce the problem in the below example. Run as a normal .Net Core 2.0 Console application.

class Class2
{

    static void Main(string[] args)
    {
        Task.Run(MainAsync);
        Console.WriteLine("Press any key...");
        Console.ReadKey();
    }

    static async Task MainAsync()
    {
        await StartAsync();
        //await Task.Delay(1);  //a little delay makes it working
        Stop();
    }


    static async Task StartAsync()
    {
        var tcs = new TaskCompletionSource<object>();
        StartCore(tcs);
        await tcs.Task;
    }


    static void StartCore(TaskCompletionSource<object> tcs)
    {
        _cts = new CancellationTokenSource();
        _thread = new Thread(Worker);
        _thread.Start(tcs);
    }


    static Thread _thread;
    static CancellationTokenSource _cts;


    static void Worker(object state)
    {
        Console.WriteLine("entering worker");
        Thread.Sleep(100);  //some work

        var tcs = (TaskCompletionSource<object>)state;
        tcs.SetResult(null);

        Console.WriteLine("entering loop");
        while (_cts.IsCancellationRequested == false)
        {
            Thread.Sleep(100);  //some work
        }
        Console.WriteLine("exiting worker");
    }


    static void Stop()
    {
        Console.WriteLine("entering stop");
        _cts.Cancel();
        _thread.Join();
        Console.WriteLine("exiting stop");
    }

}

What I'd expect is the complete sequence as follows:

Press any key...
entering worker
entering loop
entering stop
exiting worker
exiting stop

However, the actual sequence stalls on the Thread.Join call:

Press any key...
entering worker
entering stop

Finally, if I insert a small delay in the MainAsync body, everything goes fine. Why (where) the deadlock happens?

NOTE: in the original code I solved using a SemaphoreSlim instead of a TaskCompletionSource, and there's no problems at all. I only would like to understand where the problem is.

Mario Vernari
  • 6,649
  • 1
  • 32
  • 44
  • 1
    Any reason why you're mixing `Task`s with `Thread`s? – vgru Dec 20 '17 at 09:08
  • Change to `var tcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);` and it will run as you expect. Now `SetResult` runs continuations synchronously, and continuation includes call to `Stop()`, which itself joins thread in which `SetResult` was executed, so they deadlock. – Evk Dec 20 '17 at 09:12
  • @Groo in the original code there's a long-running thread (the worker) which performs several jobs without mixing tasks. However, there's an awaitable function exposed to start the worker/thread which completes when some initial job has been done. – Mario Vernari Dec 20 '17 at 09:14

2 Answers2

3

tcs.SetResult(null); call in Worker() will not return until the underlying task is finished (check this question for details). In you case the task status is WaitingForActivation that's why you get a deadlock:

  1. Thread executing Worker() is blocked by tcs.SetResult(null) call.

  2. Thread executing Stop() is blocked by _thread.Join() call.

CodeFuller
  • 30,317
  • 3
  • 63
  • 79
0

Because MainAsync() thread is 'faster' than the other thread. And you're doing control only on tasks not on threads!

In your method MainAsync() you await method StartAsync() to finish its work and then you start the thread. Once method StartAsync() is done with its work (created and started thread) that function informs MainAsync() about finishing its work. Then MainAsync() calls Stop method. But where is your thread? It runs in parallel without any control and tries to finish its work. This isn't deadlock, there is no synchronization between task and thread.

Thats why when you put await Task.Delay(1) your code works because thread is enough fast to finish work before task end it (thread.join).

Palle Due
  • 5,929
  • 4
  • 17
  • 32
NSKBpro
  • 373
  • 1
  • 14
  • hmm....first off, the StartAsync ends when the TaskCompletionSource will be released by the SetResult method, and that's inside the thread. Secondly, the thread should NOT end unless the token will be marked as cancelled (and that's in the Stop method). – Mario Vernari Dec 20 '17 at 09:10