0

В продолжение вопроса, который я задал пару дней назад:

я пытаюсь сделать остановку - возобновление работы задачи (Task).

Пример я написал на WinForms, но вообще не важно, как именно - мне просто хочется понять, как работать с задачей. Но, как я понимаю, если задача перешла в состояние cancelled, то возобновить её выполнение не получается.

У меня в примере - форма с двумя кнопками, и текстбоксом, куда просто печатаются числа.

И старт-стоп работает только один раз.

Как это сделать более правильно?

Спасибо!

    public partial class Form1 : Form
    {
        public Form1()
        {
            InitializeComponent();
            token = cancelTokenSource.Token;
            startBtn.Click += StartBtn_Click;
            stopBtn.Click += StopBtn_Click;
        }
    CancellationTokenSource cancelTokenSource = new CancellationTokenSource();
    CancellationToken token;
    int cnt = 0;

    private void StartBtn_Click(object? sender, EventArgs e) {
        var t = Task.Run(() => { do { ModifyTextBox(); Thread.Sleep(1000); } while (!token.IsCancellationRequested); }, token);
    }

    private void StopBtn_Click(object? sender, EventArgs e) {
        cancelTokenSource.Cancel();
    }

    private void ModifyTextBox() {
        Action modAction = () =>
        {
            textBox1.Text += (++cnt).ToString() + Environment.NewLine;
        };

        if (textBox1.InvokeRequired)
            textBox1.Invoke(modAction);
        else
            modAction();
    }
}

Продолжение

я попробовал учесть рекомендации и написал такой код. Теперь я всю работу вынес в Action, а на основе Action стараюсь каждый раз создать при нажатии на кнопку НОВУЮ задачу. Даже объект task сделал локальным.

И всё таки получаю сообщение об ошибке 'Start may not be called on a task that has completed' при попытке второй раз нажать на startBtn.

    public partial class Form1 : Form
    {
        public Form1()
        {
            InitializeComponent();
            token = cancelTokenSource.Token;
            startBtn.Click += StartBtn_Click;
            stopBtn.Click += StopBtn_Click;
        myAction = () =>
        {
            do {
                ModifyTextBox();
                Thread.Sleep(1000);
            } while (!token.IsCancellationRequested);
        };

    }

    CancellationTokenSource cancelTokenSource = new CancellationTokenSource();
    CancellationToken token;
    int cnt = 0;
    Action myAction;

    private async void StartBtn_Click(object? sender, EventArgs e) {
        Task task = new Task(myAction, token);
        task.Start();
        await task;
        task.Dispose();
        task = null;
    }



    private void StopBtn_Click(object? sender, EventArgs e) {
        cancelTokenSource.Cancel();
    }

    private void ModifyTextBox() {
        Action modAction = () =>
        {
            textBox1.Text += (++cnt).ToString() + Environment.NewLine;
        };

        if (textBox1.InvokeRequired)
            textBox1.Invoke(modAction);
        else
            modAction();
    }
}

Третй и четвертый вариант: после советов, которые мне дали в комментариях, я всё переписал под создание новой задачи при каждом нажатии на кнопку Start.

И передо мной был выбор: как же прерывать задачу? Есть два способа - просто return'ом после того, как стало истинным token.IsCancellationRequested или взывая token.ThrowIfCancellationRequested();

К сожалению, магия работает только в руках волшебников: при первом способе - задача во второй раз не запускается с сообщением "A task was canceled".

    public partial class Form1 : Form
    {
        public Form1()
        {
            InitializeComponent();
            startBtn.Click += StartBtn_Click;
            stopBtn.Click += StopBtn_Click;
        token = cancelTokenSource.Token;
        myAction = () => {
            do {
                ModifyTextBox();

                if (token != null && token.IsCancellationRequested)
                    return;

                Thread.Sleep(1000);
            } while (true);
        };
    }

    CancellationTokenSource cancelTokenSource = new CancellationTokenSource();
    CancellationToken token;
    int cnt = 0;
    Action myAction;

    private async void StartBtn_Click(object? sender, EventArgs e) {
        await Task.Run(myAction, token);
    }


    private void StopBtn_Click(object? sender, EventArgs e) {
        cancelTokenSource.Cancel();
    }

    private void ModifyTextBox() {
        Action modAction = () =>
        {
            textBox1.Text += (++cnt).ToString() + Environment.NewLine;
        };

        if (textBox1.InvokeRequired)
            textBox1.Invoke(modAction);
        else
            modAction();
    } 
}

при втором способе при повторном нажатии на кнопку Start задача просто не продолжает работу. Ну, то есть, один раз кнопка срабаотывает - и всё.

Возможно, я не понимаю какой то тонкий момент, связанный со стартом задачи?

    public partial class Form1 : Form
    {
        public Form1()
        {
            InitializeComponent();
            startBtn.Click += StartBtn_Click;
            stopBtn.Click += StopBtn_Click;
        token = cancelTokenSource.Token;
        myAction = () => {
            do {
                ModifyTextBox();

                if (token != null)
                    token.ThrowIfCancellationRequested();

                Thread.Sleep(1000);
            } while (true);
        };
    }

    CancellationTokenSource cancelTokenSource = new CancellationTokenSource();
    CancellationToken token;
    int cnt = 0;
    Action myAction;

    private async void StartBtn_Click(object? sender, EventArgs e) {
        try {
            await Task.Run(myAction, token);
        }
        catch(OperationCanceledException ex) {

        }
    }


    private void StopBtn_Click(object? sender, EventArgs e) {
        cancelTokenSource.Cancel();
    }

    private void ModifyTextBox() {
        Action modAction = () =>
        {
            textBox1.Text += (++cnt).ToString() + Environment.NewLine;
        };

        if (textBox1.InvokeRequired)
            textBox1.Invoke(modAction);
        else
            modAction();
    } 
}

M.O.
  • 53
  • ИМХО, вы не должны хотеть делать такое тасками. Хотите паузу - завершите задачу, сохраните состояние, освободите ресурсы. Не, ну если хотите, запихните семафор и в случае, когда нужна пауза, вызывайте его, обратное действие делайте в случае возобновления. – EvgeniyZ Sep 10 '23 at 14:33
  • 2
    В старые добрые времена, когда настоящие потоки (Thread) ещё не были преданы анафеме, такая проблема решалась элементарно: поток можно было приостановить (Suspend), а потом продолжить его исполнение (Resume). Раньше было лучше! (tm) – Alexander Petrov Sep 10 '23 at 14:45
  • Спасибо, но не совсем понимаю вот что: задачи - это как бы очень "легковестная" сущность, и создавать их можно по любой необходимости. Но объекты я могу создавать много раз подряд, А с задачами у меня этот фокус не получается. Как же правильно создавать несколько задач? – M.O. Sep 10 '23 at 14:48
  • 1
    public async Task MyTask(){ //Делаем что-то }, далее await MyTask();, готово, вот вам каждый раз новая задача, которая выполняет то, что внутри. Суть задачи - выполнить что-то асинхронно, не более. – EvgeniyZ Sep 10 '23 at 14:53
  • @EvgeniyZ я попробовал дописать в StartBtn_Click() после строки с Task.Run следующее: await task; task.Dispose(); task = null; - но всё равно, при попытке повторно стартовать задачу я натыкаюсь на то, что 'A task was canceled' - то есть, НОВАЯ задача не создаётся. А нет для задач такого же new(), как для объектов? – M.O. Sep 10 '23 at 15:27
  • 1
    Не await task, а повторно task = new Task(myAction, token); await task;, ну или await Task.Run(...);. Вы не делаете новую задачу, вы все еще используете старую. – EvgeniyZ Sep 10 '23 at 16:09
  • @EvgeniyZ - я постарался всё сделать так, как Вы посоветовали. Единственная разница - я стартовал только что созданную задачу при помощи task.Start(). И всё равно - мне это не помогло. Возможно, я Вас не так понял... Картина такая: есл я прерываю задачу просто проверкой token.IsCancellationRequested и return-ом - задача в следующий раз не стартует из аз того, что "A task was cancelled". Еслии же я пытаюсь вызывать token.ThrowIfCancellationRequested - то задача молча не стартует во второй раз. Сейчас подробно опишу в вопросе... – M.O. Sep 10 '23 at 17:13
  • 2
    Вы делаете яичницу, берете сковородку, масло, разбили первое яйцо, затем взяли новое и разбили его, опять взяли новое и разбили, и так до тех пор, пока не наберете нужное кол-во. Вопрос: Вы можете взять уже разбитое яйцо и положить его как 2-е? Весьма странная задумка, верно? Ну вот таски это тоже самое. Таск - это одна конкретная задача, вы не должны их хранить гдет, вы не должны их перезапускать, вы должны каждый раз брать и запускать новую задачу, с новым токеном отмены, и так далее. – EvgeniyZ Sep 10 '23 at 17:23
  • О, а последний комментарий помог! – M.O. Sep 10 '23 at 17:33
  • 1
    Если анализировать ваш код, то вы делаете не то изначально. Обновление UI элементов не должно по сути быть в задаче, это не асинхронная задача. Вот допустим, нам надо вывести на экран текущее время, которое раз в секунду будет обновляться в UI, мы делаем простую Task с передачей прогресса async Task Run(IProgress<DateTime> progress){ progress.Report(DateTime.Now); await Task.Delay(1000); }, далее за пределами делаем var progress = new Progress<DateTime>(time => TextBox.Text = time.ToString());, ну и запускаем задачу, передав туда progress, готово. Как видите Task просто дает результат. – EvgeniyZ Sep 10 '23 at 17:52
  • 1
    Ну а если нам не нужен прогресс, а просто результат, то await Task<int> Run() { await Task.Delay(1000); return 1; }, а дальше просто var result = await Run(); TextBox.Text = result.ToString();. Разделите ответственность, не мешайте все в одну кучу) – EvgeniyZ Sep 10 '23 at 17:53

2 Answers2

1

по моему вы полезли... слишком далеко и не туда. надо вам анализировать состояние паузы - так добавьте соответствующее поле/свойство. надо анализировать это свойство для каждого потока - вынесите всё это в отдельный класс. в общем в двух словах (и читая задание по диагонали), как то так:

    private void Stop_Click(object sender, RoutedEventArgs e)
    {
        isworking = false;
    }
private void Pause_Click(object sender, RoutedEventArgs e)
{
    ispaused = !ispaused;
}

private void Start_Click(object sender, RoutedEventArgs e)
{
    isworking = true;
    System.Threading.Tasks.Task t = new System.Threading.Tasks.Task(processing);
    t.Start();
}

int counter = 0;
bool isworking = false;
bool ispaused = false;
object locker = &quot;&quot;;
private void processing()
{
    while (isworking)  {
        System.Threading.Thread.Sleep(1000);
        if(!ispaused) {
            lock (locker) {
                counter++;
                Dispatcher.BeginInvoke(new System.Threading.ThreadStart(() =&gt; {
                    lbl1.Content = counter.ToString();
                }));
            }
        }
    }
}

  • собственно на "старт" добавляется новая задача. одна-две-тышча. на "пауза" все задачи приостанавливаются. или привозобновляются. на "стоп" все задачи сбрасываются. но... там косяк: чтоб задачи сбросились надо чтоб пауза больше секунды продержалась. да и в текущем виде после "стопа" каждая задача по разику увеличит счётчик. – Spectral Owl Sep 11 '23 at 13:57
  • 1
    А мне кажется, не туда пошли и вы тоже) Вместо того, чтобы создать полноценную задачу, асинхронную задачу, вы намешали все, и потоки, и локи, и диспетчер, когда как это решается стандартными инструментами в пару строк ( привет семафор). – EvgeniyZ Sep 11 '23 at 14:29
  • так диспетчер - для возврата в базовый поток (тут не по теме, т.к. вриант для WPF, ну да ладно). локи - выполняют синхронизации потоков (собственно, семафор предназначен для того-же). А синхронно-асинхронно... конкретно в этом примере - разницы нет, имхо. Итого - то же самое решение в пару строк (остальное обвязка - обработчики кнопок), но без использования "стандартного" (когда он успел таким стать?) семафора. И да. лучше - можно. Всегда. Но, не правильнее ли будет это лучше продемонстрировать, чем просто упомянуть что оно есть?) тем более автору нужно не конкретное решение, а пример чегонить – Spectral Owl Sep 12 '23 at 07:59
  • Диспетчер вам не нужен. 2. Локи делают асинхронное синхронным, они тут совсем не нужны. 3. Семафоры были почти всегда в .NET из коробки. 4. Семафоры не для "синхронизации" созданы, а для ограничения кол-ва (что как-раз в этой задачи и требуется). 5. "разницы нет" - разница в том, что у вас потоки, когда речь про таски (асинхронность), которых у вас практически нет....
  • – EvgeniyZ Sep 12 '23 at 11:45