计划和取消任务列表

时间:2018-10-22 14:33:54

标签: c# .net xamarin xamarin.forms

我有一个自定义窗口小部件,必须启动计划的Task对象的列表,为简单起见,让我们以Xamarin的“文本到语音”示例为例。

现在,我想安排一个演讲,等待五秒钟,然后再开始一个演讲。唯一的问题是我不知道该怎么做。而且,我必须能够一次全部取消它们。

迭代1:Task.ContinueWith

编辑:根据建议,我正在将Task.ContinueWith与一个取消令牌一起使用:

    public CancellationTokenSource cancel_source;
    public CancellationToken cancel_token;      

    public async void Play_Clicked(object sender, System.EventArgs e)
    {
        if (!is_playing)
        {
            System.Diagnostics.Debug.Print("start");

            is_playing = true;

            cancel_source = new CancellationTokenSource();
            cancel_token = cancel_source.Token;

            current_task =
                Task.Factory.StartNew(
                    async () =>
                    {
                        System.Diagnostics.Debug.Print("first task");
                        await DependencyService.Get<ITextToSpeech>().SpeakAsync("Wait for five seconds...", cancel_source, cancel_token);
                    }
                ).ContinueWith(
                    async (arg) =>
                    {
                        System.Diagnostics.Debug.Print("wait task");
                        await Task.Delay(5000, cancel_token);
                    }
                ).ContinueWith(
                    async (arg) =>
                    {
                        System.Diagnostics.Debug.Print("last task");
                        await DependencyService.Get<ITextToSpeech>().SpeakAsync("You waited!", cancel_source, cancel_token);
                    }
                ).ContinueWith(
                    async (arg) =>
                    {
                        System.Diagnostics.Debug.Print("All done!");
                        await Task.Delay(100);
                    }
            );

            await current_task;
        }
        else
        {
            System.Diagnostics.Debug.Print("stop");

            //foreach (var p in l)   <----------------- will bother about canceling next, not right now
            //{
            //    if (p.task.IsCompleted) continue;

            //    DependencyService.Get<ITextToSpeech>().CancelSpeak();
            //    p.source.Cancel();
            //}

            is_playing = false;

            //DependencyService.Get<ITextToSpeech>().CancelSpeak();
            //cancel_source.Cancel();
            //cancel_source = null;
            //current_task = null;
        }
    }

我实现的方法很奇怪,当我单击按钮时,它只是说“等待5秒钟”,而当我再次单击时,它说的是第二部分。

我的实现如下:

public class TextToSpeechImplementation : ITextToSpeech
{
    public AVSpeechSynthesizer speechSynthesizer;
    public AVSpeechUtterance speechUtterance;
    public TaskCompletionSource<bool> tcsUtterance;
    public CancellationTokenSource cancel_source;
    public CancellationToken cancel_token;

    public async Task SpeakAsync(string text, CancellationTokenSource source, CancellationToken token)
    {
        cancel_source = source;
        cancel_token = token;

        tcsUtterance = new TaskCompletionSource<bool>();

        System.Diagnostics.Debug.Print("START ASYNC IMPLEMENTATION {0}", System.DateTime.Now.ToString("HH:mm:ss"));

        var now = System.DateTime.Now;

        speechSynthesizer = new AVSpeechSynthesizer();
        speechUtterance = new AVSpeechUtterance(text);

        speechSynthesizer.DidFinishSpeechUtterance += (sender, e) => System.Diagnostics.Debug.Print("STOP ASYNC IMPLEMENTATION {0} duration {1}", System.DateTime.Now.ToString("HH:mm:ss"),
                                                                                                    (System.DateTime.Now - now).TotalSeconds);

        speechSynthesizer.DidCancelSpeechUtterance += (sender, e) => System.Diagnostics.Debug.Print("SPEECH CANCELED");

        speechSynthesizer.SpeakUtterance(speechUtterance);

        await tcsUtterance.Task;
    }

    public void CancelSpeak()
    {
        speechSynthesizer.StopSpeaking(AVSpeechBoundary.Immediate);
        tcsUtterance.TrySetResult(true);
        cancel_source.Cancel();
    }
}

我看到计划的任务几乎同时运行,所以我只得到“等待5秒钟”,然后什么都没有(任务显然已经完成运行)。

有任何提示吗?

迭代2:生成任务

总是感谢Ryan Pierce Williams,我已经修改了课程,现在唯一真正的问题是如何取消即将执行/当前任务的列表。

工作负载的接口现在创建了一个新的Text-To-Speech类实例,该实例取自Xamarin的教程(我仍然想简单一点!),如下所示:

public interface ITextToSpeech
{
    ITextToSpeech New(string text, CancellationTokenSource source, CancellationToken token);
    void Speak(string text);
    Task SpeakAsync(string text);
    void CancelSpeak();
}

public class TextToSpeechImplementation : ITextToSpeech
{
    public string speech_text;
    public AVSpeechSynthesizer speechSynthesizer;
    public AVSpeechUtterance speechUtterance;
    public TaskCompletionSource<bool> tcsUtterance;
    public CancellationTokenSource cancel_source;
    public CancellationToken cancel_token;

    public ITextToSpeech New(string text, CancellationTokenSource source, CancellationToken token)
    {
        speech_text = text;
        cancel_source = source;
        cancel_token = token;

        speechSynthesizer = new AVSpeechSynthesizer();
        speechUtterance = new AVSpeechUtterance(speech_text);
        speechSynthesizer.DidFinishSpeechUtterance += (sender, e) => System.Diagnostics.Debug.Print("STOP IMPLEMENTATION {0}", System.DateTime.Now.ToString("HH:mm:ss"));
        speechSynthesizer.DidCancelSpeechUtterance += (sender, e) => System.Diagnostics.Debug.Print("SPEECH CANCELED");

        return this;
    }

    public void Speak(string text)
    {
        System.Diagnostics.Debug.Print("START IMPLEMENTATION {0}", System.DateTime.Now.ToString("HH:mm:ss"));
        speechSynthesizer.SpeakUtterance(speechUtterance);
    }

    public async Task SpeakAsync(string text)
    {
        System.Diagnostics.Debug.Print("START ASYNC IMPLEMENTATION {0}", System.DateTime.Now.ToString("HH:mm:ss"));
        tcsUtterance = new TaskCompletionSource<bool>();
        speechSynthesizer.SpeakUtterance(speechUtterance);
        await tcsUtterance.Task;
    }

    public void CancelSpeak()
    {
        speechSynthesizer.StopSpeaking(AVSpeechBoundary.Immediate);
        tcsUtterance?.TrySetResult(true);
        cancel_source.Cancel();
    }
}

小部件类现在仅对工作负载使用同步调用,因为我在其中生成不需要的任务async

    public bool is_playing;
    public CancellationTokenSource cancel_source;
    public CancellationToken cancel_token;
    public List<string> l;

    public PlayerWidget(int category, int book)
    {
        is_playing = false;
        l = new List<string>();
        cancel_source = new CancellationTokenSource();
        cancel_token = cancel_source.Token;
    }

    public void Play_Clicked(object sender, System.EventArgs e)
    {
        if (!is_playing)
        {
            System.Diagnostics.Debug.Print("start");

            is_playing = true;

            l.Clear();
            l.Add("Wait for five seconds...");
            l.Add("You waited!");
            l.Add("and the last one is here for you.");
            l.Add("Just kidding, my man, you have this last sentence here and shall be perfectly said. Now I have to go... so... farewell!");

            var state = new TaskState()
            {
                Delay = 1000,
                CancellationToken = cancel_token,
                Workload = DependencyService.Get<ITextToSpeech>().New(l[0], cancel_source, cancel_token)
            };

            Task.Factory.StartNew(TaskExecutor, state, cancel_token).ContinueWith(TaskComplete);
        }
        else
        {
            // THIS DOES NOT WORK
            System.Diagnostics.Debug.Print("stop");
            is_playing = false;
            cancel_source.Cancel();
        }
    }

    public void TaskExecutor(object obj)
    {
        var state = (TaskState)obj;

        System.Diagnostics.Debug.Print("Delaying execution of Task {0} for {1} [ms] at {2}", state.TaskId, state.Delay, System.DateTime.Now.ToString("HH:mm:ss"));

        state.CancellationToken.ThrowIfCancellationRequested();

        // Delay execution, while monitoring for cancellation
        // If Task.Delay isn't responsive enough, use something like this.
        var sw = System.Diagnostics.Stopwatch.StartNew();
        while (sw.Elapsed.TotalMilliseconds < state.Delay)
        {
            Thread.Yield(); // don't hog the CPU
            state.CancellationToken.ThrowIfCancellationRequested();
        }
        System.Diagnostics.Debug.Print("Beginning to process workload of Task {0} '{1}' at {2}", state.TaskId, l[state.TaskId], System.DateTime.Now.ToString("HH:mm:ss"));

        state.Workload.Speak(l[state.TaskId]);
    }

    void TaskComplete(Task parent)
    {
        var state = (TaskState)parent.AsyncState;

        try
        {
            parent.Wait();
            System.Diagnostics.Debug.Print("Task {0} successfully completed processing its workload without error at {1}", state.TaskId, System.DateTime.Now.ToString("HH:mm:ss"));
        }
        catch (TaskCanceledException)
        {
            System.Diagnostics.Debug.Print("The Task {0} was successfully cancelled at {1}", parent.AsyncState, System.DateTime.Now.ToString("HH:mm:ss"));

            // since it was cancelled, just return. No need to continue spawning new tasks.
            return;
        }
        catch (Exception ex)
        {
            System.Diagnostics.Debug.Print("An unexpected exception brought Task {0} down. {1} at {2}", state.TaskId, ex.Message, System.DateTime.Now.ToString("HH:mm:ss"));
        }

        if (state.TaskId == l.Count - 1)
        {
            is_playing = false;
        }
        else
        {
            // Kick off another task...
            var child_state = new TaskState()
            {
                Delay = 5000,
                CancellationToken = cancel_token,
                Workload = DependencyService.Get<ITextToSpeech>().New(l[state.TaskId + 1], cancel_source, cancel_token)
            };
            Task.Factory.StartNew(TaskExecutor, child_state, cancel_token).ContinueWith(TaskComplete);
        }
    }

现在,它像一个超级按钮一样工作,可以正确地计划时间,并且可以执行工作负载。很好。

现在的问题是:如何取消任务?我需要停止当前正在播放的TTS,并阻止创建其他任何任务。我认为,调用cancel_source.Cancel();就足够了,但事实并非如此,正如您从日志中看到的那样:

start
Delaying execution of Task 0 for 1000 [ms] at 10:21:16
Beginning to process workload of Task 0 'Wait for five seconds...' at 10:21:17
START IMPLEMENTATION 10:21:17
Task 0 successfully completed processing its workload without error at 10:21:17
Delaying execution of Task 1 for 5000 [ms] at 10:21:17
2018-10-24 10:21:17.565591+0200 TestTasks.iOS[71015:16136232] SecTaskLoadEntitlements failed error=22 cs_flags=200, pid=71015
2018-10-24 10:21:17.565896+0200 TestTasks.iOS[71015:16136232] SecTaskCopyDebugDescription: TestTasks.iOS[71015]/0#-1 LF=0
STOP IMPLEMENTATION 10:21:19
Beginning to process workload of Task 1 'You waited!' at 10:21:22
START IMPLEMENTATION 10:21:22
Task 1 successfully completed processing its workload without error at 10:21:22
Delaying execution of Task 2 for 5000 [ms] at 10:21:22
Thread started: <Thread Pool> #6
STOP IMPLEMENTATION 10:21:23
Beginning to process workload of Task 2 'and the last one is here for you.' at 10:21:27
START IMPLEMENTATION 10:21:27
Task 2 successfully completed processing its workload without error at 10:21:27
Delaying execution of Task 3 for 5000 [ms] at 10:21:27
stop
An unexpected exception brought Task 3 down. One or more errors occurred. at 10:21:27
STOP IMPLEMENTATION 10:21:29
start
An unexpected exception brought Task 4 down. One or more errors occurred. at 10:21:34
stop
start
An unexpected exception brought Task 6 down. One or more errors occurred. at 10:21:39

我简单而幼稚的代码实际上并没有立即停止当前正在播放的文本,它一直持续到完成TTS并停止产生所有其他任务为止。但是,如果我再次单击“播放”按钮,任务将不会再次开始,并且如您所见,我对生成新任务有奇怪的错误。

我还是新手,该怎么办?

迭代3:单个任务,多个令牌

与往常一样,Ryan的建议非常有帮助,现在我已经成功编写了一个几乎可以正常工作的非常基本的任务处理程序:

    public void Play_Clicked(object sender, System.EventArgs e)
    {
        l.Clear();
        l.Add("Wait for five seconds...");
        l.Add("You waited!");
        l.Add("and the last one is here for you.");
        l.Add("Just kidding, my man, you have this last sentence here and shall be perfectly said. Now I have to go... so... farewell!");

        System.Diagnostics.Debug.Print("click handler playing {0}", is_playing);

        try
        {
            if (!is_playing)
            {
                System.Diagnostics.Debug.Print("start");

                cancel_source = new CancellationTokenSource();
                cancel_token = cancel_source.Token;
                current_task = new Task(SingleTask, cancel_token);
                current_task.Start();
                is_playing = true;
            }
            else
            {
                System.Diagnostics.Debug.Print("stop");

                is_playing = false;
                cancel_token.ThrowIfCancellationRequested();
                cancel_source.Cancel();
                cancel_token.ThrowIfCancellationRequested();
                current_speaker.CancelSpeak();
                cancel_token.ThrowIfCancellationRequested();
            }
        }
        catch(Exception)
        {
            System.Diagnostics.Debug.Print("cancel");

            cancel_source.Cancel();
            current_speaker.CancelSpeak();
            is_playing = false;
        }
    }

处理程序定义如下:

    public void SingleTask()
    {
        System.Diagnostics.Debug.Print("Single task started at {0}", System.DateTime.Now.ToString("HH:mm:ss"));

        foreach(var p in l)
        {
            System.Diagnostics.Debug.Print("Waiting 5s");

            //cancel_token.ThrowIfCancellationRequested();

            var sw = System.Diagnostics.Stopwatch.StartNew();
            while (sw.Elapsed.TotalMilliseconds < 5000)
            {
                Thread.Yield(); // don't hog the CPU
                //cancel_token.ThrowIfCancellationRequested();
            }

            current_speaker = DependencyService.Get<ITextToSpeech>().New(p, cancel_source, cancel_token);

            try
            { 
                System.Diagnostics.Debug.Print("Single task speaking at {0} sentence '{1}'", System.DateTime.Now.ToString("HH:mm:ss"), p);

                current_speaker.Speak(p);

                while (current_speaker.IsPlaying())
                {
                    Thread.Yield();
                }
            }
            catch (Exception)
            {
                System.Diagnostics.Debug.Print("Single task CANCELING at {0}", System.DateTime.Now.ToString("HH:mm:ss"));

                cancel_source.Cancel();
                current_speaker.CancelSpeak();
            }
        }
        System.Diagnostics.Debug.Print("Single task FINISHED at {0}", System.DateTime.Now.ToString("HH:mm:ss"));
        is_playing = false;
    }

现在,任务可以安排,执行并可以多次工作。现在问题是取消

行之有效的:在TTS朗读中句时终止任务。它奇怪地称其为“停止”和“取消”,但是它起作用:

click handler playing True
stop
cancel
2018-10-29 12:35:37.534358+0100[85164:17740514] [AXTTSCommon] _BeginSpeaking: couldn't begin playback
SPEECH CANCELED

什么不起作用:在等待下一个短语时终止任务。在等待期间,它再次调用“停止”和“取消”,但是如您所见,它会继续使用 next 语句继续运行,然后按我的意图停止(单击再按一次)。

click handler playing False
start
Single task started at 12:36:56
Waiting 5s
Single task speaking at 12:37:01 sentence 'Wait for five seconds...'
START IMPLEMENTATION 12:37:01
STOP IMPLEMENTATION 12:37:02
Waiting 5s
Thread finished: <Thread Pool> #34
Thread started: <Thread Pool> #37
click handler playing True
stop
cancel
Single task speaking at 12:37:07 sentence 'You waited!'
START IMPLEMENTATION 12:37:07
STOP IMPLEMENTATION 12:37:08

我确实相信我在这里错过了很小的一块!

最终解决方案

这是Ryan建议的最终代码,现在可以正常运行,停止中途讲话,等待时停止任务,这是我所需要的。对于后代来说,棘手的部分是在这里混合了一个任务和本机任务(TTS依赖项服务),但是现在我认为它更简洁明了:

    public void Play_Clicked(object sender, System.EventArgs e)
    {
        l.Clear();
        l.Add("Wait for five seconds...");
        l.Add("You waited!");
        l.Add("and the last one is here for you.");
        l.Add("Just kidding, my man, you have this last sentence here and shall be perfectly said. Now I have to go... so... farewell!");

        System.Diagnostics.Debug.Print("click handler playing {0}", is_playing);

        if (!is_playing)
        {
            System.Diagnostics.Debug.Print("start");

            cancel_source = new CancellationTokenSource();
            cancel_token = cancel_source.Token;
            current_task = new Task(SingleTask, cancel_token);
            current_task.Start();
            is_playing = true;
        }
        else
        {
            System.Diagnostics.Debug.Print("stop");

            is_playing = false;
            cancel_source.Cancel();
            current_speaker.CancelSpeak();
        }
    }

    public void SingleTask()
    {
        System.Diagnostics.Debug.Print("Single task started at {0}", System.DateTime.Now.ToString("HH:mm:ss"));

        foreach(var p in l)
        {
            System.Diagnostics.Debug.Print("Waiting 5s");

            var sw = System.Diagnostics.Stopwatch.StartNew();
            while (sw.Elapsed.TotalMilliseconds < 5000)
            {
                Thread.Yield(); // don't hog the CPU

                if (cancel_source.IsCancellationRequested)
                {
                    cancel_source.Cancel();
                    current_speaker.CancelSpeak();
                    return;
                }
            }

            current_speaker = DependencyService.Get<ITextToSpeech>().New(p, cancel_source, cancel_token);

            try
            { 
                System.Diagnostics.Debug.Print("Single task speaking at {0} sentence '{1}'", System.DateTime.Now.ToString("HH:mm:ss"), p);

                current_speaker.Speak(p);

                while (current_speaker.IsPlaying())
                {
                    Thread.Yield();
                }
            }
            catch (Exception)
            {
                System.Diagnostics.Debug.Print("Single task CANCELING at {0}", System.DateTime.Now.ToString("HH:mm:ss"));

                cancel_source.Cancel();
                current_speaker.CancelSpeak();
            }
        }
        System.Diagnostics.Debug.Print("Single task FINISHED at {0}", System.DateTime.Now.ToString("HH:mm:ss"));
        is_playing = false;
    }

1 个答案:

答案 0 :(得分:1)

在.NET的任务库中,取消被视为异步任务必须响应的请求(例外:如果尚未开始运行的计划任务检测到已请求取消,则框架可能会将其取消)。

为了让任务检查是否已请求取消,您必须将CancellationToken传递给任务。这可以作为可选状态参数(或作为其一部分)完成。以下是一个任务示例,该任务将无限循环直到请求取消:

Sub Main()
    Dim cts As New CancellationTokenSource()
    Dim ct = cts.Token
    Dim t = Task.Factory.StartNew(AddressOf InfiniteLoop, ct, ct)

    Thread.Sleep(5000)
    Console.WriteLine("Task Status after 5000 [ms]: {0}", t.Status)
    Debug.Assert(t.Status = TaskStatus.Running)

    cts.Cancel()
    Try
        t.Wait()
    Catch ex As Exception
        Console.WriteLine("ERROR: {0}", ex.Message)
    End Try

    Console.WriteLine("Task Status after cancelling: {0}", t.Status)
    Console.WriteLine("Press enter to exit....")
    Console.ReadLine()
End Sub

Public Sub InfiniteLoop(ByVal ct As CancellationToken)
    While True
        ct.ThrowIfCancellationRequested()
    End While
End Sub

对于同步执行任务,只需维护一个工作队列(ConcurrentQueue)。对运行的每个任务都使用Task.ContinueWith(...),以便它可以启动队列中的下一个项目(或取消所有项目)。

您可以使用Task.Delay(5000)启动一个需要5秒才能完成的任务。使用Task.Delay(5000).ContinueWith(myTask)延迟执行任务。

编辑:按照您的描述方式,听起来您只是想不断生成新任务,直到有人告诉您停止。我在下面编写了一个示例应用程序,即可完成该任务:

using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;

namespace TaskQueueExample
{
class Program
{
    public class TaskState
    {
        private static int _taskCounter = 0;

        public int TaskId { get; set;  }
        public int Delay { get; set; }
        public int Workload { get; set; }
        public CancellationToken CancellationToken { get; set; }

        public TaskState()
        {
            TaskId = _taskCounter;
            _taskCounter++;
        }
    }

    static CancellationTokenSource _cts = new CancellationTokenSource();
    static Random _rand = new Random();

    static void Main(string[] args)
    {
        var state = new TaskState() { Delay = _rand.Next(0, 1000), Workload= _rand.Next(0, 1000), CancellationToken = _cts.Token };

        Task.Factory.StartNew(Program.DoSomeWork, state, _cts.Token).ContinueWith(Program.OnWorkComplete);

        Console.WriteLine("Tasks will start running in the background. Press enter at any time to exit.");
        Console.ReadLine();

        _cts.Cancel();
    }

    static void DoSomeWork(object obj)
    {
        if (obj == null) throw new ArgumentNullException("obj");
        var state = (TaskState)obj;

        Console.WriteLine("Delaying execution of Task {0} for {1} [ms]", state.TaskId, state.Delay);

        state.CancellationToken.ThrowIfCancellationRequested();

        // Delay execution, while monitoring for cancellation
        // If Task.Delay isn't responsive enough, use something like this.
        var sw = Stopwatch.StartNew();
        while(sw.Elapsed.TotalMilliseconds < state.Delay)
        {
            Thread.Yield(); // don't hog the CPU
            state.CancellationToken.ThrowIfCancellationRequested();
        }

        Console.WriteLine("Beginning to process workload of Task {0}", state.TaskId);

        // Simulate a workload (NOTE: no Thread.Yield())
        sw.Restart();
        while(sw.Elapsed.TotalMilliseconds < state.Workload)
        {                
            state.CancellationToken.ThrowIfCancellationRequested();
        }           
    }

    static void OnWorkComplete(Task parent)
    {
        var state = (TaskState)parent.AsyncState;

        try
        {
            parent.Wait();
            Console.WriteLine("Task {0} successfully completed processing it's workload without error.", state.TaskId);
        }
        catch(TaskCanceledException)
        {
            Console.WriteLine("The Task {0} was successfully cancelled.", parent.AsyncState);

            // since it was cancelled, just return. No need to continue spawning new tasks.
            return;
        }
        catch(Exception ex)
        {
            Console.WriteLine("An unexpected exception brought Task {0} down. {1}", state.TaskId, ex.Message);
        }

        // Kick off another task...
        var child_state = new TaskState() { Delay = _rand.Next(0, 1000), Workload = _rand.Next(0, 1000), CancellationToken = _cts.Token };
        Task.Factory.StartNew(Program.DoSomeWork, child_state, _cts.Token).ContinueWith(Program.OnWorkComplete);
    }


}
}