代码之家  ›  专栏  ›  技术社区  ›  senseiwa

计划和取消任务列表

  •  1
  • senseiwa  · 技术社区  · 7 年前

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

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

    迭代1: 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:生成任务

    多亏了瑞安·皮尔斯·威廉姆斯,我已经修改了课程,现在唯一真正的问题是如何 取消 即将开始/当前任务的列表。

    现在,工作负载的接口创建了一个新的文本到语音类实例,该实例取自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在句子中间时终止任务。它奇怪地同时调用“stop”和“cancel”,但它工作:

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

    什么不起作用 :等待下一个短语时终止任务。在等待期间,它会再次调用“stop”和“cancel”,但正如您所看到的,它会继续 下一个 句子,然后按我的意愿停止(再次单击按钮时,它会正确地重新开始)。

    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 回复  |  直到 7 年前
        1
  •  1
  •   Ryan Pierce Williams    7 年前

    在.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);
        }
    
    
    }
    }
    
    推荐文章