如何处理用户可能点击按钮的情况,该按钮会多次调用长时间运行的异步操作。
我的想法是先检查异步操作是否正在运行,取消它并重新启动它。
到目前为止,我已尝试使用CancellationTokenSource构建此类功能,但它无法按预期工作。有时会运行两个异步操作,因此当我启动新的async操作时,“旧的”异步操作不会被取消,这会混淆结果处理。
如何处理此类情况的任何建议或示例?
public async void Draw()
{
bool result = false;
if (this.cts == null)
{
this.cts = new CancellationTokenSource();
try
{
result = await this.DrawContent(this.TimePeriod, this.cts.Token);
}
catch (Exception ex)
{}
finally
{
this.cts = null;
}
}
else
{
this.cts.Cancel();
this.cts = new CancellationTokenSource();
try
{
result = await this.DrawContent(this.TimePeriod, this.cts.Token);
}
catch (Exception ex)
{}
finally
{
this.cts = null;
}
}
}
编辑: 最后,我认为在短时间内运行两个异步操作(当新的被激活但旧的尚未取消时)并不坏。
这里真正的问题是我如何显示最终用户的进度。当旧的异步操作结束时,它会从最终用户隐藏进度指示器,但新触发的异步操作仍在运行。
EDIT2: 在DrawContent内部(...)我使用ThrowIfCancellationRequested,因此取消正在运行的任务似乎工作正常。
关于进度显示。当调用Draw()时,我将加载指示器设置为可见,当此方法结束时,我隐藏加载指示器。所以现在当我开始新的操作后取消之前的异步操作时,我的加载指示器被设置为隐藏。如果在“旧”方法结束时还有其他异步方法仍在运行,我应该如何跟踪。
答案 0 :(得分:3)
我想抓住机会改进some related code。在您的情况下,它可以像下面这样使用。
注意,如果挂起操作的上一个实例失败(抛出OperationCanceledException
以外的任何内容),您仍会看到错误消息。这种行为很容易改变。
如果在操作结束时它仍然是最新的任务实例,它只会隐藏进度UI:if (thisTask == _draw.PendingTask) _progressWindow.Hide();
此代码不是线程安全的(_draw.RunAsync
不能同时调用),并且旨在从UI线程调用。
Window _progressWindow = new Window();
AsyncOp _draw = new AsyncOp();
async void Button_Click(object s, EventArgs args)
{
try
{
Task thisTask = null;
thisTask = _draw.RunAsync(async (token) =>
{
var progress = new Progress<int>(
(i) => { /* update the progress inside progressWindow */ });
// show and reset the progress
_progressWindow.Show();
try
{
// do the long-running task
await this.DrawContent(this.TimePeriod, progress, token);
}
finally
{
// if we're still the current task,
// hide the progress
if (thisTask == _draw.PendingTask)
_progressWindow.Hide();
}
}, CancellationToken.None);
await thisTask;
}
catch (Exception ex)
{
while (ex is AggregateException)
ex = ex.InnerException;
if (!(ex is OperationCanceledException))
MessageBox.Show(ex.Message);
}
}
class AsyncOp
{
Task _pendingTask = null;
CancellationTokenSource _pendingCts = null;
public Task PendingTask { get { return _pendingTask; } }
public void Cancel()
{
if (_pendingTask != null && !_pendingTask.IsCompleted)
_pendingCts.Cancel();
}
public Task RunAsync(Func<CancellationToken, Task> routine, CancellationToken token)
{
var oldTask = _pendingTask;
var oldCts = _pendingCts;
var thisCts = CancellationTokenSource.CreateLinkedTokenSource(token);
Func<Task> startAsync = async () =>
{
// await the old task
if (oldTask != null && !oldTask.IsCompleted)
{
oldCts.Cancel();
try
{
await oldTask;
}
catch (Exception ex)
{
while (ex is AggregateException)
ex = ex.InnerException;
if (!(ex is OperationCanceledException))
throw;
}
}
// run and await this task
await routine(thisCts.Token);
};
_pendingCts = thisCts;
_pendingTask = Task.Factory.StartNew(
startAsync,
_pendingCts.Token,
TaskCreationOptions.None,
TaskScheduler.FromCurrentSynchronizationContext()).Unwrap();
return _pendingTask;
}
}
答案 1 :(得分:0)
调用cts.Cancel()不会自动停止任务。您的任务需要主动检查是否已请求取消。你可以这样做:
public async Task DoStuffForALongTime(CancellationToken ct)
{
while (someCondition)
{
if (ct.IsCancellationRequested)
{
return;
}
DoSomeStuff();
}
}
答案 2 :(得分:0)
为什么不遵循BackgroundWorker模式并在DrawContent中跳出循环?
private bool _cancelation_pennding=false;
private delegate DrawContentHandler(TimePeriod period, Token token)
private DrawContentHandler _dc_handler=null;
.ctor(){
this._dc_handler=new DrawContentHandler(this.DrawContent)
}
public void CancelAsync(){
this._cancelation_pennding=true;
}
public void Draw(){
this._dc_handler.BeginInvoke(this.TimePeriod, this.cts.Token)
}
private void DrawContent(TimePeriod period, Token token){
loop(){
if(this._cancelation_pennding)
{
break;
}
//DrawContent code here
}
this._cancelation_pennding=false;
}