5

У меня есть thread с циклом на 15 элементов и для эмитации работы я делаю sleep на секунду. Я сделала логику которая если юзер нажимает на кнопку повторно, то предыдущий поток должен быть остановлен и новый должен начать работу. Для этого я создала флаг m_isShouldStop и когда юзер повторно нажимает кнопку, то флаг меняется на true и следом вызываю Wait и ожидаю, что предыдущий поток остановиться, дождется Wait, создаст новый поток и начнет выполнение заново. Но происходит следующее - я вижу, что флаг меняется, но в потоке флаг как будто остается тем же(как будто работает с его копией) и Wait не выходит он просто зависает там.

Вот код:

 private Task m_exeTask;
 private static bool m_isShouldStop = false;
 private TaskFactory FactoryTask { get; set; }

public void Start(int count) { if (m_exeTask != null) { m_isShouldStop = true; m_exeTask.Wait(); m_exeTask.Dispose(); m_exeTask = null; m_isShouldStop = false; }

        var uiContext = TaskScheduler.FromCurrentSynchronizationContext();

        m_exeTask = FactoryTask?.StartNew(() =>
        {
           if(m_isShouldStop ){
                  return;
             }
            Console.WriteLine("HERE start the loop");

            for (int i = 0; i < count; i++)
            {
               Thread.Sleep(1000);    //WORK EMULATION
                Dispatcher.Invoke(new Action(() =>
                {
                //Here I do some UI stuff
                Console.WriteLine("HERE start NEXT");
                }), DispatcherPriority.ContextIdle);
            }

            Console.WriteLine("HERE end the loop");

        }, tokenSource.Token, TaskCreationOptions.None, uiContext);
    }

    private void Button_Click(object sender, RoutedEventArgs e)
    {
        Start(15);
    }
}

Что делаю не так?

ПРАВКА в итоге получилось так, код работает. Если есть рекомендации, пишите :-)

 CancellationTokenSource cancelTokenSource = new CancellationTokenSource();
    public async void Start(int count)
    {
        if (m_exeTask != null)
        {
            cancelTokenSource.Cancel();
            await m_exeTask;
            m_exeTask.Dispose();
            m_exeTask = null;
            cancelTokenSource = new CancellationTokenSource();
        }
        var uiContext = TaskScheduler.FromCurrentSynchronizationContext();
        m_exeTask = FactoryTask?.StartNew(() =>
        {
            Console.WriteLine("HERE start the loop");

            for (int i = 0; i < count; i++)
            {
                if (cancelTokenSource.Token.IsCancellationRequested)
                {
                    Console.WriteLine("IT WAS CANCELED");
                    break;
                }
                else
                {
                    Thread.Sleep(1000);    //WORK EMULATION
                    Dispatcher.Invoke(new Action(() =>
                    {
                        //Here I do some UI stuff
                        Console.WriteLine("HERE start NEXT");
                    }), DispatcherPriority.ContextIdle);
                }
            }
            Console.WriteLine("HERE end the loop");
        }, tokenSource.Token, TaskCreationOptions.None, uiContext);
    }

Teti
  • 85
  • 2
    Когда будете проверять флаг в потоке добавьте к нему volatile – Roman-Stop RU aggression in UA Mar 18 '21 at 12:51
  • 2
    У вас в воркере нет проверки значения m_isShouldStop, поэтому он и продолжает выполняться. – Uranus Mar 18 '21 at 12:52
  • К уже сказанному добавлю - не путайте потоки и Task-и, это не совсем одно и то же ) – CrazyElf Mar 18 '21 at 12:54
  • @Uranus извините, скопировала не тот кусочек, в моем коде эта проверка есть, отредактировала код, можете пожалуйста еще раз посмотреть. – Teti Mar 18 '21 at 12:57
  • @CrazyElf но на сколько я понимаю в моем случае не критично :-) – Teti Mar 18 '21 at 12:58
  • @Teti Ну как не критично. Для работы с Task есть свои шаблоны проектирования, которые сильно отличаются от того, как работали с потоками когда-то в древности :) – CrazyElf Mar 18 '21 at 13:01
  • 1
    @Teti, теперь у вас проверка делается только один раз при старте воркера. Чтобы воркер завершал работу, он должен проверять время от времени. – Uranus Mar 18 '21 at 13:05
  • Удалось разобраться? – aepot Mar 18 '21 at 16:10
  • 1
    @aepot Вроде как получилось, добавила правку в вопрос, если есть рекомендации, буду рада услышать! спасибо за помощь. – Teti Mar 19 '21 at 14:56
  • FactoryTask?.StartNew заменить просто на Task.Run, он будет использовать правильный шедьюлер для выполнения на потоке из пула, так что его не надо будет задавать. CancellationTokenSource является IDisposable, ему надо высвобождать ресуры, нужен либо using либо Dispose(), посмотрите ссылку в моем ответе на пример. При использовании async void есть риск словить невидимое исключение, то есть оно уронит вам поток, но вы его не увидите, оберните код в try-catch и почитайте в статье по ссылке из ответа про асинхронность, чем опасен async void. – aepot Mar 19 '21 at 15:03
  • Dispatcher.Invoke(new Action(() => { })) можно упростить как Dispatcher.Invoke(() => { }). Кстати, про async void много всякого понаписано, поищите, почитайте, вам это пригодится. – aepot Mar 19 '21 at 15:04
  • m_exeTask.Dispose(); - а вот это лишнее. Если вы не используете Task.WaitHandle явно (а вы не используете), то таск не нужно диспозить, хуже оно конечно не делает, но и лучше тоже. – aepot Mar 19 '21 at 15:10

1 Answers1

6

Представьте себе ситуацию, когда вы вызвали Wait(), при этом поток еще продолжает выполняться. Сам Wait() уже заблокировал UI поток и он ждет.

Далее, в выполняемом потоке у вас вызывается Dispatcher.Invoke, то есть выполнение фрагмента кода в UI потоке, но он уже выполняет вот прямо сейчас Wait(), то есть заблокирован, при этом ваш поток ждет, когда Dispatcher.Invoke отработает, а Dispatcher.Invoke ждет, когда отработает Wait(), чтобы выполнить отправленный в него фрагмент кода.

И получается ситуация, когда UI поток ждет завершения сторого потока, а второй поток не может завершиться, пока UI поток не выполнит для него код. И вот они ждут друг друга...и ждут...и будут вечно ждать, пока вы не убьете приложение.

И такая смертельная для приложения взаимоблокировка потоков называется дэдлок - Deadlock.

Вылечить можно любым из способов:

  1. Выполнить Console.WriteLine() прямо в потоке без вызова диспетчера
  2. Вызвать диспетчер асинхронно, то есть BeginInvoke вместо Invoke, разница в том, что первый не будет ждать, пока отработает код в UI потоке
  3. Ожидать завершения задачи асинхронно (не блокируя UI поток), то есть await m_exeTask вместо m_exeTask.Wait() - Асинхронное программирование

Добавлю только что вместо bool, которая не является потокобезопасной, ибо может быть кеширована в потоке, надежнее использовать volatile bool, а еще лучше - познакомиться с тем, как используется CancellationToken (вот пример его использования).


Так же проверку на выход стоит делать прямо в цикле

for (int i = 0; i < count; i++)
{
    if (m_isShouldStop)
        return;
    // ...
}
aepot
  • 49,560
  • 1
    Да уж, лучше сразу CancellationToken использовать по прямому назначению, чем вот это вот всё ) – CrazyElf Mar 18 '21 at 13:00
  • @CrazyElf так написал же, и даже ссылку на пример воткнул :) Но использование CTS не освобождает от ответственности за неправильное ожидание завершения потока. – aepot Mar 18 '21 at 13:00
  • 1
    Да у меня не к вам претензия, просто в коде вопроса такого намешано... Прям на ностальгию пробивает, сам когда-то так писал, но это было очень давно )) – CrazyElf Mar 18 '21 at 13:03