异步搜索具有CancellationToken的文件



我正在尝试构建一个函数,该函数在目录中搜索文件并将它们添加到ObservableCollection中。

此任务应该异步运行,以便UI保持响应,并在再次执行该方法时随时停止搜索。使得CCD_ 2被清除并且搜索再次开始。

我的问题是,当任务仍在运行时,我不知道如何以及何时以相同的方法取消CancellationTokenSource
此外,我不知道如何在函数上使用Async和Await,因为它分为三个方法。

现在看起来是这样的:

搜索功能:

CancellationTokenSource _tokenSource;
public async void SearchAsync()
{
Files.Clear();
FileCount = 0;
// Cancels Task if its running
if(_tokenSource != null)
{
_tokenSource.Cancel(); (throws Error right here. Task already disposed)
}
// Create new Cancellation Token
_tokenSource = new CancellationTokenSource();
CancellationToken token = _tokenSource.Token;
try
{
//Starts Task
await Task.Run(() => FindFiles(token));
}
catch (OperationCanceledException ex)
{
//When Canceled
MessageBox.Show("Test", "Test");
}
finally
{
//When finished
_tokenSource.Dispose();
}
}

查找文件:

private async Task FindFiles(CancellationToken token)
{
foreach (string filePath in Directory.EnumerateFiles(MainPath, "*.*", searchOption))
{
await AddFileAsync(filePath);
FileCount++;
if (token.IsCancellationRequested)
{
Files.Clear();
FileCount = 0;
token.ThrowIfCancellationRequested();
}
}
}

将文件添加到ObservableCollection:

我将其放入自己的方法中,因为它在LoadFiles()中被多次调用。我只是简化了它。

public ObservableCollection<FileModel> Files { get; set; }
private async Task AddFileAsync(string filePath)
{
await Application.Current.Dispatcher.BeginInvoke(DispatcherPriority.Background, new Action(() =>
{
FileModel file = new()
{
FileName = Path.GetFileName(filePath),
FilePath = filePath
};
Files.Add(file);
}));
}

您的代码有几个问题:

  1. 您将处理CancellationTokenSource,但在下一次调用SearchAsync()时,您将在已处理的CTS上调用.Cancel()。这个呼叫当然失败了
  2. FindFiles()中使用async void。你的await Task.Run(() => FindFiles(token));会立即退出。除非绝对必要,否则永远不要使用async void
  3. 您可能有一个竞争条件,这取决于您的任务调度程序是否是多线程的

看看这个实现:

public sealed class Searcher
{
// Initialized here to avoid null checks.
private CancellationTokenSource _tokenSource = new CancellationTokenSource();
public async Task SearchAsync()
{
var newTokenSource = new CancellationTokenSource();
var oldTokenSource = Interlocked.Exchange(ref _tokenSource, newTokenSource);
if (!oldTokenSource.IsCancellationRequested)
{
oldTokenSource.Cancel();
}
try
{
//Starts Task
await Task.Run(() => FindFiles(newTokenSource.Token));
}
catch (OperationCanceledException)
{
//When Canceled
}
}
private async Task FindFiles(CancellationToken token)
{
await Task.Delay(1000, token);
}
}

因此,现在对SearchAsync()的每次调用都会跟踪两个CTS:当前CTS和前一个CTS。这解决了问题1。

我把async void FindFiles变成了async Task FindFiles解决了问题2。我还将SearchAsync()的签名更改为async Task,以便调用者可以等待它。尽管这取决于函数的使用方式,并且不是必需的。

最后,我使用Interlocked.Exchange来解决问题3。此调用是原子调用。另一种选择是使用锁。

我已经删除了CTS处理代码,因为这并不容易。主要问题是,当您完成CTS时,应该调用.Dispose()。因此,无论是在任务被取消时,还是在任务完成时。问题是,您无法轻松验证CTS是否已被处理(之后它处于不可用状态(。或者我不知道该怎么做。处理CTS既奇怪又困难,请阅读以下内容:何时处理CancellationTokenSource?那里的大多数答案都声称,如果CTS没有链接,那么GC会在某个时候正确地收集和处理它。

一种方法是用一个自定义类包装CTS,该类跟踪该CTS是否已被释放。并且对.Cancel().Dispose()的调用可能是用锁包装的。但后来一切都变得复杂起来。另一种选择是在访问ObjectDisposedException时捕获它,但我不确定这是否正确。

另一方面,对于这个简单的用例,我认为你不必担心处理它。让GC收集它并处理它。CTS和令牌都不会泄漏到外部,如果你记得保持等待,那应该没问题。我会根据这个注释来做这件事:

注意

在发布对CancellationTokenSource的最后一个引用之前,请始终调用ObservableCollection0。否则,在垃圾收集器调用CancellationTokenSource对象的Finalize方法之前,它正在使用的资源将不会被释放。

在我看来,如果不是绝对需要立即处理,那么不处理它是可以的。事实并非如此

最新更新