Что вызывает тупик?

Я столкнулся с проблемой тупика в части моего кода. К счастью, мне удалось воспроизвести проблему в приведенном ниже примере. Запустите как обычное консольное приложение .Net Core 2.0.

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");
    }

}

Я ожидаю, что полная последовательность будет следующей:

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

Однако фактическая последовательность останавливается на вызове Thread.Join:

Press any key...
entering worker
entering stop

Наконец, если я вставлю небольшую задержку в тело MainAsync, все пойдет нормально. Почему (где) происходит взаимоблокировка?

ПРИМЕЧАНИЕ: в исходном коде я решил использовать SemaphoreSlim вместо TaskCompletionSource, и проблем не возникло. Я только хотел бы понять, где проблема.


person Mario Vernari    schedule 20.12.2017    source источник
comment
По какой причине вы смешиваете Task с Thread?   -  person Groo    schedule 20.12.2017
comment
Измените на var tcs = new TaskCompletionSource<object>(TaskCreationOptions.RunContinuationsAsynchronously);, и он будет работать так, как вы ожидаете. Теперь SetResult запускает продолжения синхронно, а продолжение включает вызов Stop(), который сам присоединяется к потоку, в котором выполнялся SetResult, поэтому они блокируются.   -  person Evk    schedule 20.12.2017
comment
@Groo в исходном коде есть продолжительный поток (рабочий), который выполняет несколько заданий, не смешивая задачи. Тем не менее, существует ожидаемая функция для запуска рабочего потока/потока, которая завершается, когда выполняется какое-либо начальное задание.   -  person Mario Vernari    schedule 20.12.2017


Ответы (2)


tcs.SetResult(null); вызов в Worker() не вернется, пока основная задача не будет завершена (проверьте это вопрос для подробностей). В вашем случае статус задачи WaitingForActivation, поэтому вы получаете тупик:

  1. Поток, выполняющий Worker(), заблокирован вызовом tcs.SetResult(null).

  2. Поток, выполняющий Stop(), заблокирован вызовом _thread.Join().

person CodeFuller    schedule 20.12.2017
comment
Спасибо за помощь! - person Mario Vernari; 20.12.2017

Потому что поток MainAsync() "быстрее", чем другой поток. И вы контролируете только задачи, а не потоки!

В вашем методе MainAsync() вы ожидаете, что метод StartAsync() завершит свою работу, а затем запускаете поток. Как только метод StartAsync() завершил свою работу (создал и запустил поток), эта функция информирует MainAsync() о завершении своей работы. Затем MainAsync() вызывает метод Stop. Но где твоя нить? Он работает параллельно без какого-либо контроля и пытается завершить свою работу. Это не тупик, нет синхронизации между задачей и потоком.

Вот почему, когда вы ставите await Task.Delay(1), ваш код работает, потому что поток достаточно быстр, чтобы закончить работу до завершения задачи (thread.join).

person NSKBpro    schedule 20.12.2017
comment
хм .... во-первых, StartAsync заканчивается, когда TaskCompletionSource будет выпущен методом SetResult, и это внутри потока. Во-вторых, поток НЕ должен заканчиваться, если токен не будет помечен как отмененный (и это в методе Stop). - person Mario Vernari; 20.12.2017