Правильно отмените асинхронную операцию и запустите ее снова
Как обрабатывать случай, когда пользователь может нажать кнопку, которая запускает длительную асинхронную операцию, несколько раз.
Моя идея состояла в том, чтобы сначала проверить, выполняется ли асинхронная операция, отменить ее и запустить снова.
До сих пор я пытался создать такую функциональность, используя CancellationTokenSource, но она не работает должным образом. Иногда выполняется две асинхронные операции, поэтому "старые" асинхронные операции еще не отменяются, когда я запускаю новую, и это смешивает обработку результатов.
Какие-нибудь предложения или примеры, как обращаться с этим случаем?
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;
}
}
}
РЕДАКТИРОВАТЬ: В конце концов, я думаю, это не плохо, что за короткое время выполняются две асинхронные операции (когда запускается новая, но старая еще не отменена).
Настоящая проблема здесь в том, как я показываю прогресс для конечного пользователя. Когда старая асинхронная операция завершается, она скрывает индикатор выполнения от конечного пользователя, но вновь запущенная асинхронная операция все еще выполняется.
РЕДАКТИРОВАТЬ 2: Внутри DrawContent(...) Я использую ThrowIfCancellationRequested, поэтому отмена запущенного задания, кажется, работает нормально.
О показе прогресса. Когда вызывается Draw(), я устанавливаю индикатор загрузки видимым, а когда этот метод заканчивается, я скрываю индикатор загрузки. Так что теперь, когда предыдущая асинхронная операция отменяется после запуска новой, мой индикатор загрузки становится скрытым. Как я должен следить, если еще один асинхронный метод все еще работает, когда "старый" заканчивается.
3 ответа
Я хотел бы воспользоваться возможностью улучшить некоторый связанный код. В вашем случае его можно использовать, как показано ниже.
Обратите внимание, что если предыдущий экземпляр ожидающей операции завершился неудачно (выдается что-то кроме OperationCanceledException
), вы все равно увидите сообщение об ошибке. Это поведение может быть легко изменено.
Он только скрывает пользовательский интерфейс выполнения, если к концу операции, если это все еще самый последний экземпляр задачи: if (thisTask == _draw.PendingTask) _progressWindow.Hide();
Этот код не является потокобезопасным, как есть (_draw.RunAsync
не может быть вызван одновременно) и предназначен для вызова из потока пользовательского интерфейса.
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;
}
}
Почему бы не следовать шаблону 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;
}
Вызов cts.Cancel() не остановит задачу автоматически. Ваша задача должна активно проверять, была ли отменена заявка. Вы можете сделать что-то вроде этого:
public async Task DoStuffForALongTime(CancellationToken ct)
{
while (someCondition)
{
if (ct.IsCancellationRequested)
{
return;
}
DoSomeStuff();
}
}