【发布时间】:2019-03-26 16:13:58
【问题描述】:
我有一个自定义小部件,它必须启动一个计划的Task 对象的列表,为简单起见,让我们以 Xamarin 的 Text-To-Speech 为例。
现在我想安排一次演讲,等待五秒钟,然后开始另一场演讲。唯一的问题是我不知道该怎么做。而且,我必须能够一次全部取消。
迭代 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,我已经修改了课程,现在唯一真正的问题是如何取消即将到来/当前任务的列表。
工作负载接口现在创建了一个取自 Xamarin 教程的 Text-To-Speech 类的新实例(我仍然想简单地玩一下!),如下所示:
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;
}
【问题讨论】:
-
嘿@senseiwa,当您在取消后恢复您尝试恢复您的任务时,您将需要一个新的 CancellationTokenSource(旧的已被取消,因此使用它构建的任何未来任务都会自动取消)。
-
@RyanPierceWilliams 谢谢,我现在很接近了!我已根据您的建议编辑了代码,现在它几乎可以工作了。它会产生一个任务,说话,并取消中间的句子,如果我再次点击,它会重新启动。唯一不起作用的部分是它等待下一句时。像往常一样,非常欢迎任何提示,您让我了解一卡车的任务/线程!谢谢!
-
很高兴它有帮助 :) 至于您剩下的问题,我已经查看了您更新的代码,并且立即弹出给我的是您正在对取消令牌执行检查错误的位置。这些检查应该在任务线程中进行,而不是在您的 UI 线程中。 UI 线程应该告诉 CancellationTokenSource 实例取消,而任务应该检查从 CancellationTokenSource 检索到的提供的 CancellationToken 以查看是否已请求取消。
-
另外,如果您不希望在任务线程中引发异常,您可以随时使用 IsCancellationRequested 属性,它只会给您一个布尔值。
-
它有效@RyanPierceWilliams,谢谢!我已经编辑了这个问题,标记为已解决,如果你在我的代码中看到一些可疑的东西,请毫不犹豫地告诉我。感谢您的大力帮助,混合任务、本机任务和处理所有启动/停止/取消事件并不容易。谢谢!
标签: c# .net xamarin xamarin.forms